Compare commits

..
35 Commits
Author SHA1 Message Date
Bitsy c76f629971 feat(bridge): expose the material3-adaptive version to consumers
material3-adaptive rides its own release train upstream (1.3.0-rc01),
like material3 does, so a consumer had no way to express its version
through the plugin - they would have had to hardcode a number and keep
it in sync with the port by hand.

Adds composeDesktopNative.composeMaterial3Adaptive alongside the existing
compose / composeMaterial3 / composeRuntime accessors, stamped into
bridge-version.properties from the version catalog like the others.

The README consumer snippet now shows the adaptive coordinates, and calls
out that window-core has to be declared DIRECTLY when commonMain touches
WindowSizeClass - substituted modules hide their transitives from the
common metadata classpath.
2026-09-22 16:52:25 +02:00
Bitsy c4564b3a10 feat(demo): make the Adaptive screen fold on a live resize
The first version of this screen proved the vendored adaptive modules
render, but it did not really demonstrate adaptation: it left
rememberListDetailPaneScaffoldNavigator on its default directive, which
is the WINDOW size class, and pinned the scaffold to a fixed 420dp.

The window is the wrong input here. The screen does not get the window -
the demo shell spends 190dp on the sidebar plus 48dp of padding - so
defaulting folds ~240dp too late and leaves two panes side by side in a
region already too cramped for them. The directive is now computed from
the width the screen actually gets (BoxWithConstraints), which is what
any pane-hosting app that is not full-window should do; the pane height
tracks the window height instead of being a constant. The header prints
the window size class and the pane-area size class side by side so the
difference is visible while dragging.

Also adds selection highlighting and a Back button that only appears
while folded, so the single-pane mode is actually navigable.

New `--adaptivetest` probe: drives one window wide -> narrow -> wide with
setSize and asserts the scaffold folds and unfolds. This is what proves
the resize chain end to end - SDL resize, the owner's snapshot-backed
containerSize, recomposition, BoxWithConstraints re-measure, a new
PaneScaffoldDirective on the navigator, a new scaffold value. The
--screen=Adaptive --width=N screenshots could only ever prove the fold
was right for the size the window OPENED at, since each run is a fresh
process. The probe reads a small internal seam on the screen
(gAdaptiveTwoPane); it is documented as a probe hook and is not UI.

Verified on mingwX64: folds at 800dp and stays two-pane at 1100/1400dp,
--adaptivetest passes repeatedly, and the JVM parity leg plus commonMain
metadata still compile.
2026-09-22 16:46:57 +02:00
Bitsy fb8e97b74d feat(material3): port material3-adaptive to desktop native
Closes the request in issue #4. org.jetbrains.compose.material3.adaptive:*
1.3.0-rc01 publishes ios_arm64 / ios_simulator_arm64 / macos_arm64 and
nothing else - no linux, no mingw. (It does carry macosArm64, which
answers the reporter's open question.)

All four artifacts are now vendored and built for every desktop-native
target, with ZERO source edits. Two things made it a pure re-target:

- every platform actual the native legs need already exists upstream:
  adaptive/nonAndroidMain (calculatePosture), adaptive-layout/skikoMain
  (~75 l10n tables + Strings + the drag handle) and
  adaptive-layout/nativeMain (identityHashCode, isGraphicsLayerElement)
- androidx.window:window-core - the dependency `adaptive` api-exposes and
  the obvious suspect for the apple-only publication - has shipped
  linux_x64 / linux_arm64 / mingw_x64 / macos_arm64 klibs all along, under
  BOTH the google and jetbrains coordinates

So the artifacts were missing only because upstream never enabled the
targets, not because anything blocked them.

125 files vendored off the existing COMPOSE_CORE_REF pin - no new
SET_REPO. Upstream's skikoMain lands in src/vendor/native/ beside
upstream's own nativeMain; the two actualise disjoint expects so they
share one source set.

Both bridges substitute all four coordinates, so consumers keep declaring
the official org.jetbrains.compose.material3.adaptive:* coords.

Verified: all four compile for mingwX64 + linuxX64 + linuxArm64, API
dumps generated, vendor-clean and audit-exclusions pass, and
`demo --screen=Adaptive` renders a live ListDetailPaneScaffold with the
window size class resolving correctly. adaptive-navigation3 builds and
dumps API but has no demo screen yet, so its nav3 SceneStrategy path is
unproven at runtime.

Two notes for later:
- the new demo screen hit the granular-metadata trap documented in
  CLAUDE.md: because `adaptive` is substituted on native configs, KGP
  drops its transitives from the commonMain metadata classpath, so
  window-core had to be declared DIRECTLY in demo's commonMain
- demo's JVM parity leg moves navigation3-ui 1.2.0-alpha02 -> 1.1.2 to
  follow CMP 1.12.1's navigation3 reset onto the 1.1 line
2026-09-22 16:07:23 +02:00
Bitsy 6c3bfd8679 chore(deps): coil 3.6.2 -> 3.6.3, ktor 3.5.2 -> 3.6.0
Sweep of every pin the project owns. Only these two moved:
- coil: COIL_REF bumped with it and the vendored sources re-synced (no
  local edits needed)
- ktor: plain Maven dep, apidemo's HTTP client

Already latest, left alone: SDL3 (release-3.4.16), koin 4.2.2, okio
3.18.2, coroutines 1.11.0, kotlinx-io-okio 0.9.1. Deliberately not
bumped: kotlinx-serialization (latest is 1.12.0-RC, a prerelease) and
everything under policy 1, which CMP 1.12.1 still pins where we are.

The pulse group rename also lands in the klib dumps here - the unique
name embeds the group, and BCV infers the macOS dumps from the checked-in
files on Windows, so those headers had to be rewritten for apiDump to run.
2026-09-22 15:51:26 +02:00
Bitsy ec2d9b06ad chore(pulse): namespace the republish under the project group
Pulse is the one vendored dependency that is ITSELF a com.bitsycore
library (upstream com.bitsycore.lib:*), so republishing it as a bare
com.bitsycore.pulse:* read like an upstream coordinate and left room for
a real collision if upstream ever moves under that group.

Group becomes com.bitsycore.compose.desktop.native.pulse:*. Nothing
changes for consumers: they still declare the upstream com.bitsycore.lib
coordinate and the bridge plugin substitutes it on native configs.
2026-09-22 15:48:21 +02:00
Bitsy 1d6e5a76de chore: port to Compose Multiplatform 1.12.1
Bumps COMPOSE_CORE_REF / COMPOSE_REF to v1.12.1 and re-syncs all 1760
vendored files. No vendored file needed an edit; the ten manual vendors
were all verified unchanged upstream base..pin and only needed their
VENDOR-BASE ref re-stamped.

skiko stays 0.150.1, so the mingwX64 fork (0.150.1-mingw.2) still matches
its Skia base - no republish.

navigation3 moves BACKWARDS, 1.2.0-alpha04 -> 1.1.7: upstream re-checked
navigation3 out of release/1.11 for this patch (redirectversions.toml), so
the vendored :navigation3:navigation3-ui is now 1.1.7 source and the Maven
runtime has to follow it down. 1.1.7 publishes every target this port needs.

Two upstream-driven API deltas, both matching CMP:
- ui gains LocalSystemTheme, re-exposed upstream as @InternalComposeUiApi
- navigation3-ui loses NavigationBackHandler / rememberNavigationEventState,
  deleted by the 1.1.7 reset

LayerTransformationMatrix.kt's VENDOR-BASE path also corrected - it still
pointed at ui/ui, but upstream moved Matrices.skiko.kt to ui/ui-graphics,
which is why the drift check could not diff it.

Verified on mingwX64: both apps link, full probe suite passes (nav3test,
backtest, multiwintest included), vendor-clean and audit-exclusions clean.
2026-09-22 15:47:19 +02:00
Bitsy eb44db915e refactor: finish the SDL3* -> Sdl3* rename and regenerate the API dumps
Picks up the file/type renames (SDL3Backend -> Sdl3Backend, SDL3FrameClock ->
Sdl3FrameClock, SDL3EventMapper.kt -> AppEvent.kt) and unifies the 15 references
the rename left behind. All were prose or strings, which is why it still
compiled:

  - a user-visible error message ("SDL3Backend received GpuMode.Auto ...")
  - two user-visible demo UI strings ("driven by the SDL3FrameClock ...")
  - the MARK headers in AppEvent.kt and Sdl3Backend.kt
  - KDoc cross-references, including to `SDL3EventMapper.kt` which no longer
    exists - repointed at AppEvent.kt
  - 3 PLAN.md references

The codebase now uses Sdl3* uniformly, matching the Sdl3MainDispatcher /
Sdl3Clipboard / Sdl3DragAndDropOwner / Sdl3ScrollConfig that already followed
that form. `sdl3.*` (the cinterop package) and SDL_* (the C API) are untouched.

API dumps regenerated: a clean 1:1 rename, 41 SDL3Backend -> 41 Sdl3Backend in
:ui and nothing else - the other renamed types were not public. Breaking, but
com.compose.sdl only; no androidx.compose.* / org.jetbrains.compose.* change.

Verified beyond compiling: apiCheck green, both metadata publish gates compile,
nav3test/backtest/multiwintest PASS, raw SDL handles still resolve
(gpu=OpenGL window=true glContext=true), and the Images screen renders.
2026-09-19 00:12:36 +02:00
Bitsy 1120abc488 Update README to remove Infrastructure and add Support
Removed the Infrastructure section and added a Support section with a Discord link.
2026-09-18 23:54:32 +02:00
Bitsy 9d2656f905 refactor: make module-local project declarations internal
162 declarations leave the published API. They were public only because nothing
had ever audited the surface - none is Compose Multiplatform API, and none was
a deliberate native extra.

Removed by package:
  com.compose.sdl.graphics       45   canvas/painter/gradient/path plumbing
  com.compose.sdl.renderer.skia  23   SkiaBridge + SkiaSurfaceBridge
  com.compose.sdl.platform       21   DefaultViewConfiguration
  com.compose.sdl.res            17   ResourcePainter, vector->SVG, resource kinds
  com.compose.sdl.window         11   popup host internals
  com.compose.sdl.icons           7   IconDefaults

Method: inventory each module's public declarations from its klib dump by
DECLARATION OWNER (not by grepping names, which the earlier pass got wrong),
keep those referenced from another module, mark the rest internal, then let the
COMPILER adjudicate. That last step matters - it rejected three candidates that
look module-local but are real API, and I would have got at least one of them
wrong by reasoning alone:

  EncodedImageDecoder  exposed by a public property - it is an install seam
  RawSdlHandles        the return type of rawSdlHandles()
  ApplicationScope     the receiver of nativeComposeApp { Window(...) }

All three stay public.

Compose Multiplatform surface untouched: every removed line is owned by a
com.compose.sdl.* package, zero androidx.compose.* or org.jetbrains.compose.*.

Verified at runtime, since internalising the resource and renderer plumbing is
exactly the kind of change that compiles and then fails to draw: nav3test,
backtest and multiwintest PASS; the Images screen (which drives ResourcePainter,
the vector->SVG path and the image cache) renders 2112 distinct colours; the CPU
raster path still renders.

NOTE what this does NOT do. Kotlin `internal` is per-module and this repo spans
~20 Gradle modules, so anything crossing a module boundary must stay public -
SDL3Backend, ComposeRootHost, AppEvent and the rest of the cross-module plumbing
are still exposed. Narrowing those needs a @RequiresOptIn marker (upstream's
@InternalComposeUiApi pattern); none exists in this project yet.
2026-09-18 22:39:13 +02:00
Bitsy 48cfa5da4d docs(renderer): record why RenderBackend keeps its one-implementation interface
SkiaRenderBackend is the only implementor, which invites 'collapse the
interface, it is dual-backend residue'. It is not, and I tried it: the concrete
type lives in skikoRendererMain, BELOW nativeMain, so :desktop-native-window's
WindowInstance - which holds the renderer in nativeMain - cannot name it.
compileNativeMainKotlinMetadata fails with "Unresolved reference 'renderer'".

Same constraint as the expect fun createRenderBackend that returns it, whose
KDoc already explains its half. Documented on the interface so the next pass
does not spend the same detour.
2026-09-18 22:32:31 +02:00
Bitsy 856cbe5ef3 refactor(renderer): flatten GpuMode - every mode is Skia
GpuMode read as `Software` vs a nested `Skia.*` group, which dated from when
the alternative to Skia was SDL's own renderer. With SDL reduced to windowing
that split is actively misleading: its own KDoc said "Software - SKIA CPU
raster", so all three modes were Skia and the taxonomy implied otherwise.

  Auto / Skia.Metal / Skia.OpenGL / Software
    -> Auto / Metal / OpenGL / CpuRaster

The axis is now what it actually is: how Skia's output reaches the screen.
Added GpuMode.isGpuAccelerated so the GPU-to-CPU fallback in WindowInstance
reads as intent rather than `gpuMode is GpuMode.Skia`.

CLI compatibility kept: --gpu still accepts the retired skia.metal / skia.opengl
/ skia-* spellings alongside the bare metal / opengl / cpu / software / raster
names, so existing scripts and docs do not break.

API break is in com.compose.sdl only - no androidx.compose.* or
org.jetbrains.compose.* declaration changes, so the Compose Multiplatform
surface this port mirrors is untouched.

Verified the renderer actually still works on both paths rather than just
compiling: the resolved default reports gpu=OpenGL, every --gpu alias parses and
renders, and CPU-raster vs OpenGL screenshots of the Buttons screen differ by
0.01% of pixels (antialiasing only) with identical mean RGB.
2026-09-18 22:31:20 +02:00
Bitsy 5c7308c614 refactor(demo,renderer): split the probe suite out of MainNative; drawRoot is abstract
MainNative.kt held the entry point plus 20 probe scenarios in 1240 lines. The
scenarios now live in demo/src/nativeMain/kotlin/probes/, grouped by what they
exercise, and MainNative.kt is 284 lines of argument dispatch:

  InputProbes.kt      click, toggle, key, scroll, cursor, IME
  WindowProbes.kt     multi-window, window info, search-escape, locale
  AnimationProbes.kt  shared transition, AnimatedVisibility, dialog entry
  TextProbes.kt       font resolution, paragraph layout, metrics
  LifecycleProbes.kt  back navigation, Navigation3
  RenderProbes.kt     soak run, encoded-image decode

Behaviour is unchanged - each probe is `internal` so main()'s dispatch still
reaches it - and 134 imports that did not apply to the file they landed in were
pruned. Verified by running them, not just compiling: nav3test, backtest,
multiwintest and winattrtest still PASS, and clicktest/scrolltest (whose
scenarios moved) still report their expected values.

RenderBackend.drawRoot is now abstract rather than carrying an empty default
body. The no-op dated from when a second, non-Skia backend could opt out of it;
with SkiaRenderBackend the only implementation, an unimplemented drawRoot would
silently render nothing instead of failing to compile.

NOT done, and it should not be: removing the expect/actual createRenderBackend
seam. It looks like dual-backend residue but is load-bearing for a different
reason its KDoc already documents - the actual sits in skikoRendererMain, below
nativeMain, so without the expect it is invisible to nativeMain-level consumers.
Tested by deleting it: compileCommonMainKotlinMetadata still passed, but
:desktop-native-window's compileNativeMainKotlinMetadata failed with
"Unresolved reference 'createRenderBackend'" - and that is the compile the
WINDOWS publish job runs to produce the root modules, so it would have broken
publishing rather than local builds.
2026-09-18 22:25:58 +02:00
Bitsy 2fb322776a refactor: rename the f-prefixed member convention to m
500 occurrences across 31 files: mWidth, mSurface, mCache and friends. The `f`
prefix is retired project-wide.

Scope was checked before touching anything, because a blind rename here could
have done real damage:

- VENDORED code is untouched. All 32 src/vendor trees contain ZERO f-prefixed
  names (upstream Compose does not use the convention), so nothing in the
  byte-for-byte copies could be caught by the pattern. check-vendor-clean and
  check-vendor-drift both still pass.
- API-NEUTRAL. No f-prefixed name appears in any published .klib.api dump -
  they were all private - so this changes no consumer-visible surface, and
  apiCheck passes without a re-dump.
- The only matches inside quotes were string TEMPLATES ("...at $mPos",
  "frames=$mFrames"), i.e. code references that had to be renamed with
  everything else, not literal text.

Verified at runtime, not just by compiling: --nav3test, --backtest,
--multiwintest and --winattrtest all pass, and a Buttons screenshot still
renders.

Also corrects CLAUDE.md's Conventions section, which claimed the project used
"no f/in/v prefixes". That had not been true for a long time - the code uses v
for locals, in for parameters and k for file-level constants throughout. It now
documents what the code actually does, including that Composable parameters stay
plain because the name is part of the call-site API.
2026-09-18 22:00:40 +02:00
Bitsy 7c7bb63cae refactor(input): fold LegacyPointerEvent into AppEvent.Pointer
LegacyPointerEvent was a hand-written 4-field carrier (x, y, type, button) with
longhand equals/hashCode/toString - a data class written out by hand. Its whole
life was three lines in SDL3EventMapper that wrapped it and four in
WindowInstance that immediately took it apart again, so the indirection bought
nothing while putting the word "Legacy" on the live input path.

AppEvent.Pointer now carries the four fields directly, which also makes it
consistent with its MouseWheel sibling (already flat). LegacyPointerEvent.kt is
deleted, and com.compose.sdl.input went empty with it.

API: this removes a published declaration, so it is binary-breaking for anyone
who reached into the raw SDL event pipeline. Nothing in-tree did - demo and
apidemo never reference AppEvent or com.compose.sdl.input at all - and this
layer is internal plumbing rather than documented consumer surface. Net effect
on the klib dump is 26 declarations out, 16 in.

Verified pointer dispatch still works rather than assuming: --searchesctest
CLICKS the SearchBar to expand it, and its before/after screenshots differ by
84.87% of pixels, so the click landed and the overlay opened. --nav3test,
--backtest and --multiwintest also pass.
2026-09-18 21:42:00 +02:00
Bitsy 02fb2ddf4e fix(renderer): bind each window's GL context per frame
`demo --multiwintest` exited 139 (SIGSEGV) with no output, so the multi-window
regression probe had silently been asserting nothing.

An OpenGL context is per-WINDOW, but "current" is per-THREAD state, and
SDL_GL_MakeCurrent ran exactly once per window at creation (SDL3Backend.init)
with nothing re-binding it per frame. So with two windows open, whichever
initialised last owned the thread: window A issued its draw and swap against
window B's context. That is silent corruption while both windows live, and a
hard fault in present() the moment B is destroyed and the current context is
left dangling - which is what the probe hit.

SkiaGLBridge now binds its own context at every entry point that touches GL:
init, ensureSize (before the unchanged-size early return, so it is the
per-frame bind covering the draw phase that follows), present, and destroy (a
window frees its GPU objects against its own context, not whichever happens to
be current). Purely additive.

Verified both paths, because the single-window path is the one with the most to
lose:
  single window  Buttons screenshot mean RGB [46.6 44.4 51.5], 167 distinct
                 colours - IDENTICAL to before the change, as expected since
                 SDL short-circuits a redundant bind
  multi window   --multiwintest PASS (was exit 139)
  --nav3test, --backtest, --winattrtest all still pass

Worth recording: multi-window was never correct here, not merely broken at
teardown - two live windows were sharing one context the whole time.
2026-09-18 21:35:25 +02:00
Bitsy 126e83c056 refactor(window): split the window module; add the Compose Desktop Window attributes + a raw SDL escape hatch
ComposeWindow.kt had grown to 985 lines holding four unrelated concerns. Split
by concern, no behaviour change:

  ComposeWindow.kt    337  public surface (ApplicationScope, Window(),
                           nativeComposeApp, nativeComposeWindow) + main loop
  WindowInstance.kt   540  one SDL window: backend, render backend, root host,
                           recomposer/composition, events, frame pump, teardown
  AppRuntime.kt        82  live-window registry + create/destroy lifecycle
  FrameTiming.kt       83  virtual/deterministic frame clocks + probe quiescence
  WindowAttributes.kt  30  the new attribute bundle

Also pruned 25 imports that no longer applied to what stayed behind.

WINDOW ATTRIBUTES. Window() and nativeComposeWindow() now take the same
parameters as Compose Desktop's Window(): undecorated, transparent, resizable,
enabled, focusable, alwaysOnTop, onPreviewKeyEvent and onKeyEvent. The mutable
ones re-apply whenever they change (DisposableEffect keyed on the bundle);
transparent is creation-only because SDL needs SDL_WINDOW_TRANSPARENT at
SDL_CreateWindow and offers no setter, which WindowAttributes documents.

`enabled = false` drops pointer / key / wheel / text / drop events while the
window keeps rendering, matching Compose Desktop. Key dispatch follows
upstream's order: window preview -> focused content -> window onKeyEvent -> app
shortcut handler -> back navigation.

RAW SDL ACCESS. ComposeNativeWindow.rawSdlHandles() returns a RawSdlHandles data
class of the four opaque pointers (window / renderer / glContext / metalView) so
advanced users can call the sdl3.* cinterop directly. Named for what it is
rather than getRawSdlAccessor(): it hands back handles, not an accessor object,
and it is a snapshot - the KDoc spells out that the pointers die with the window
and which ones are non-null per resolved GpuMode.

Verified by a new `demo --winattrtest` probe, which asserts all four behaviours
rather than eyeballing them:
  ATTR      undecorated=true resizable=false alwaysOnTop=true focusable=true
  RAW       gpu=Skia.OpenGL window=true glContext=true renderer=false metalView=false
  KEYCHAIN  preview=2 onKey=2   (both window handlers saw the down/up pair)
  DISABLED  preview=2 onKey=2   (unchanged after injecting with enabled=false)

Existing probes still pass (--nav3test, --backtest, --searchesctest).

Records PLAN.md 7c: `demo --multiwintest` segfaults on mingwX64. Confirmed
PRE-EXISTING by stashing this work and reproducing on the previous commit, so
it is not fallout from the split - the probe is just silent on crash, so it had
gone unnoticed.
2026-09-18 20:54:25 +02:00
Bitsy 0c09109e6e fix(bridge): make the app-icon object a real input of the mingw link
Editing an app-icon PNG rebuilt the .ico and the windres .o and then left the
link UP-TO-DATE, so the .exe silently kept the OLD embedded icon. Reproduced on
:apidemo - generateComposeNativeIco and compileComposeNativeIconResource both
re-ran, linkDebugExecutableMingwX64 reported UP-TO-DATE, and the executable's
md5 did not move.

The link was wired with a bare `dependsOn(windresTask)`, which only ORDERS the
two. The object's path reaches the linker as a `linkerOpts` STRING, so the path
is an input but its CONTENT never is - nothing about a changed .o could
invalidate the link.

Replaced with `inputs.files(windresTask)`, which carries the task dependency AND
puts the object's content into the up-to-date check. Verified: the link now
re-runs and the .exe changes when an icon PNG is edited, in both directions.

The .rgba window-icon blobs were never affected - they go into the data.kres Zip
via `from(dir)`, and a Copy spec is already a declared input.

Also annotates the three icon task types with @DisableCachingByDefault. They
had no cacheability declaration, which failed `validatePlugins` and with it
:compose-desktop-native-bridge:build; that failure predates this change.
2026-09-12 18:44:46 +02:00
Bitsy 5804128e1d fix(bridge): aggregate dependency modules' composeResources into data.kres
In a multi-module project, resources declared in a LIBRARY module never reached
the final data.kres - only the app module's own composeResources were packaged,
so every Res.* accessor pointing at a submodule resource failed at runtime.

The Zip sourced the app's own `generated/compose/resourceGenerator/
preparedResources/<sourceSet>/composeResources`, which is per-module by
construction. Kotlin already solves this: `<target>AggregateResources` merges
this project's own composeResources with those of every dependency module into
`build/kotlin-multiplatform-resources/aggregated-resources/<target>/`, each
under its OWN `composeResources/<package>/` prefix. The plugin now reads that.

Two bugs fixed at once. The old path also forced a SINGLE resolved package onto
everything it copied, which is wrong the moment more than one module
contributes - the aggregate keeps each module's real package instead.

The aggregate directory is already rooted at `composeResources/`, so it is
copied with no `into()` remapping. Kept the old per-module wiring as a fallback
for Kotlin versions that do not register the aggregation tasks.

Verified with a throwaway library module carrying a composeResources file:
  before  data.kres had only the app's resources
  after   composeResources/<app>.generated.resources/files/app_marker.txt
          composeResources/<lib>.generated.resources/files/submodule_marker.txt
Existing single-module apps are unaffected - :demo's resources still land under
`demo.generated.resources` (it sets packageOfResClass explicitly, which the old
code also read), and its Images screen still loads PNG/JPG/SVG/XML at runtime.
2026-09-12 13:10:30 +02:00
Bitsy 04d1df58de docs(plan): record the icon-font centring fix 2026-09-12 12:32:39 +02:00
Bitsy a87ff2d7f6 fix(icons): centre the icon-font glyph so Material Symbols stop rendering low
Material Symbols icons sat 2-4px lower on native than on the JVM reference,
the offset scaling with icon size (~13% of it): eye 11px -> +2, gear/lock 16px
-> +2, download 28px -> +4. Equal top and bottom deltas with identical ink
heights, so it was a pure downward translation, not a sizing problem.

The two stacks positioned the glyph differently. The JVM actual centres it
explicitly (MaterialSymbolsIcon.jvm.kt: y = height/2 - (ascent+descent)/2),
while IconFontIcon just placed IconText in a Box with the DEFAULT TopStart
alignment. The glyph's natural text box is taller than the nominal icon size -
its height is the font's ascent+descent and Material Symbols' em box overshoots
the icon grid - so top-aligning it pushed the glyph down.

Centering the text box is equivalent to the JVM's baseline math, because
IconText sets no lineHeight and its box height therefore IS ascent+descent.

wrapContentSize(unbounded = true) is load-bearing and was the whole reason a
first attempt at this changed nothing: Modifier.size() hands the child FIXED
constraints, so the text measured at exactly `size`, nothing overflowed, and
contentAlignment had nothing to align.

Measured on apidemo, native vs jvm ink boxes:
  gear      +2/+1 -> 0/0
  download  +4/+4 -> 0/0
  lock      +2/+1 -> 0/0
  eye       +2/+2 -> +1/+1   (half-pixel rounding: an odd-height glyph centred
                              in an even box; the JVM draws at a float y where
                              native placement is integer)
Whole-window drift >32/255: 0.29% -> 0.17%.

Found by measuring rather than by eye, off the apidemo --screenshot harness.
2026-09-12 12:32:30 +02:00
Bitsy 10380e5422 fix(text): forward topRatio to skiko so inherited lineHeight centres like upstream
Text with a lineHeight much larger than its fontSize sat visibly LOW on native
compared to the JVM reference - most obvious on apidemo's SourceTag pill, whose
Text sets fontSize = 9.sp but inherits Material 3 bodyLarge's 24.sp lineHeight.

Upstream's ParagraphBuilder.skiko.kt sets `res.topRatio = topRatio` on every
SkTextStyle, sourced from LineHeightStyle.Alignment.topRatio. The port set
`ts.height` and the paragraph `heightMode` but never set topRatio, so skiko fell
back to its own default (-1 = split the extra leading proportionally to the
font's ascent:descent). Proportional splitting puts most of a large leading
ABOVE the glyphs; Material 3's type styles ask for Alignment.Center (0.5).

Fallback is Alignment.Proportional, matching upstream, so text that specifies no
lineHeightStyle keeps its current spacing. topRatio exists on both the official
skiko and the bitsycore mingw fork, so all four targets compile.

Measured on apidemo at 1240x820, native vs jvm over the whole window:
  before  2.94% of pixels differ, 2.14% by >32/255
  after   1.52% of pixels differ, 0.29% by >32/255
The pills are now pixel-identical and the residual is rasterizer AA.

This also resolves the "constant ~2px vertical offset" logged next to it: that
was the same bug compounding through stacked text rows, not a separate issue.
Before the fix, shifting the jvm capture down 2px HALVED the error; now 0px is
strictly best (0.29% vs 2.13% at +1px).

Found via the apidemo --screenshot drift check added in the previous commit.
2026-09-12 01:57:23 +02:00
Bitsy 2fe26cf7f8 docs(plan): root-cause the residual pill mis-centering to leading distribution
Aligning the default font (7a) removed the different-typeface confound - glyph
ink is now 7 rows on both stacks - but the SourceTag pill text still sits low
on native. Measured: jvm line box 24px with 10/9 gaps, native 25px with 13/7.

Decisive test: adding an explicit lineHeight = 9.sp, so there is no extra
leading to distribute, makes the two stacks PIXEL-IDENTICAL (same 14px box,
same ink rows, same 4/3 gaps). The Text only sets fontSize, so it inherits
M3 bodyLarge's 24.sp lineHeight - a line box far taller than the glyphs.

So font metrics, glyph raster and baseline placement are all correct; the
divergence is isolated to the leading-distribution path, which both adds a
pixel to the box and biases the glyphs downward. Pointed at the heightMode
branch in SkiaParagraphEngine and the per-run height multiplier, with an
explicit warning not to paper over it by pinning lineHeight at call sites.
2026-09-12 01:34:55 +02:00
Bitsy 71aab5d502 docs(bridge): correct the stated relationship between the two bridge halves
The comment added with the ecosystem rules claimed the plugin table and the
repo-internal FULL-COMMONIZATION BRIDGE must match each other exactly. They
must not, and a cross-check shows they already do not: animation-graphics,
material-ripple and ui-backhandler have plugin rules but no root rule.

That asymmetry is correct. The plugin table has to cover every coordinate the
port PUBLISHES, since a consumer can declare any of them; the root build only
needs a rule where an IN-REPO module declares a Maven coordinate in commonMain,
and in-repo modules reach those three through project(...) instead. Restated
the real invariant: a published coordinate missing from the PLUGIN table is the
error to look for.

Verified: 32 of 35 published modules are substitutable, and the 3 that are not
(sdl-core, material-symbols, desktop-native-window) are exactly the modules
with no upstream coordinate to substitute from.
2026-09-12 01:29:18 +02:00
Bitsy d19948059b fix(apidemo): align the default font across both stacks; add headless drift capture
The SourceTag pill looked vertically centred on jvm and off-centre on native.
Not a centering bug: the pill is Box(padding(vertical = 1.dp)) { Text(9.sp) }
with no centering logic, so glyph position is decided purely by font metrics -
and the two stacks were drawing it in DIFFERENT TYPEFACES. Native resolves
FontFamily.Default to the bundled NotoSans (SkiaFonts.defaultTypeface is the
fallback for every unresolved family); the jvm leg only ever registered
monoFontFamily, so Default was a system face (Segoe UI on Windows).

That made it a parity-harness defect, not a renderer bug - and a broad one,
since every Text without an explicit fontFamily was being compared against the
wrong reference.

Fixed with a defaultFontFamily expect/actual loading the bundled NotoSans on
both legs, stamped across the M3 Typography (withDefaultFontFamily) because
typography styles carry their own fontFamily and overriding LocalTextStyle
alone would not reach them. This is the same treatment :demo's parity leg has
had all along (its notoSans/notoTypography block says as much); apidemo had
simply never been given it.

Drift capture: :apidemo could not screenshot itself, so both legs now take
--screenshot=<path> and render OFFSCREEN to quiescence - native via a GPU
framebuffer readback, jvm via ImageComposeScene - with a virtual 60fps clock
and infinite animations frozen, mirroring :demo's parity path. Neither needs a
visible window (a screen-scrape approach was tried first and captured whatever
happened to be in front of the app, so it was dropped).

First measured run, 1240x820: 2.9% of pixels differ at all, 2.1% by >32/255.
Two residuals recorded in PLAN.md 7a-bis, neither a text-metrics bug: a
constant ~2px vertical offset that accounts for about half of all remaining
drift, and a Material Symbols glyph missing from the subsetted font in the
session header (tofu on jvm, nothing on native).

Bmp.kt is duplicated from :demo rather than promoted to a library module: it is
app-level tooling and :ui-graphics deliberately does not re-export skiko.
2026-09-12 01:23:34 +02:00
Bitsy 20714b3eeb chore(deps): version sweep against CMP 1.12.0; restructure the version catalog
Applies an explicit policy, now written at the top of libs.versions.toml:
anything Compose Multiplatform integrates is pinned to what CMP ships (for the
androidx mirrors, the JETPACK version CMP's release notes map its
org.jetbrains.androidx.* artifact to, since this port consumes the google
coordinates); anything CMP does not integrate is pinned to the latest release;
deliberate exceptions are allowed but must be commented.

Checked every pin against upstream rather than against memory - the CMP core
catalog at v1.12.0 plus the actual dependency declarations of the vendored
modules:

  kotlin               2.4.0        -> 2.4.20   latest; builds + links clean,
                                                no klib API diff
  okio                 3.17.0       -> 3.18.2   latest
  ktor                 3.5.1        -> 3.5.2    latest
  navigation3-runtime  1.2.0-alpha05 -> alpha04  CMP ships jb 1.2.0-alpha02 ==
                                                Jetpack alpha04; do not drift on
                                                a pre-release line
  collection 1.5.0, graphics-shapes 1.1.0, lifecycle 2.11.0, skiko 0.150.1,
  koin 4.2.2, coil 3.6.2, SDL3 3.4.16           already correct

savedstate stays at 1.5.0 as a DOCUMENTED exception: CMP builds against 1.4.0
and lifecycle 2.11.0 requires 1.4.0, but only as a MINIMUM, so 1.5.0 resolves
with no conflict and hands consumers the newer API. navigationevent stays at
1.1.2, the latest patch of the 1.1 minor CMP ships.

The catalog is now sectioned (toolchain / CMP / skia / androidx / vendored /
infrastructure) with each group carrying the reason it is pinned where it is,
so the next sweep is mechanical. README's pinned-versions tables updated to
match, including the policy statement.
2026-09-12 01:23:15 +02:00
Bitsy d029b48e10 docs(readme): add a pinned-versions table; centralise every androidx pin
The table covers the toolchain (Kotlin, Compose Multiplatform, SDL3, both skiko
builds), the Compose libraries with official-vs-port coordinates side by side,
the vendored ecosystem libraries with the reason each one is vendored, the
androidx artifacts used as-is, and the infrastructure pins.

To make that table a contract rather than prose, every androidx coordinate now
comes from the version catalog instead of being written literally in six build
files. That surfaced a real inconsistency: androidx.navigation3:navigation3-runtime
was pinned at 1.1.4 in :compose:ui:ui and demo's commonMain but 1.2.0-alpha05 in
:navigation3:navigation3-ui and demo's jvmMain. Gradle resolved the conflict to
the highest, so the build was fine, but nothing said which was intended - now
there is one entry.

Also records WHY Kotlin stays on 2.4.0 rather than dropping to the 2.2.20 that
CMP 1.12.0 is built with, and why material3 is 1.12.0-alpha03 at CMP 1.12.0.
2026-09-12 00:42:40 +02:00
Bitsy 47445ab7ab docs: document the vendored ecosystem libraries + the VENDOR-REIMPL convention
feat(bridge): substitute Koin / Coil 3 / Pulse MVI for consumers too

The consumer-facing bridge plugin's table only covered Compose, so a third-party
app declaring io.insert-koin / io.coil-kt.coil3 / com.bitsycore.lib coordinates
would have resolved the upstream artifacts and failed on mingwX64. Added all 15
entries, with a note that the plugin table and the repo-internal
FULL-COMMONIZATION BRIDGE are two halves of the same swap and must stay in sync.

Docs:
- CLAUDE.md: koin/ coil/ pulse/ in the module map (including WHY each is
  vendored and the source-set / skiko-branch shapes), the five pinned upstream
  repos, and the "check BOTH coordinate sets" rule with the duplicate-classes
  hazard that follows from it.
- CLAUDE.md: the VENDOR-REIMPL marker next to VENDOR-BASE in the vendoring
  rules - a reimplementation is deliberately not drift-tracked, only a
  SIGNATURE change matters.
- README.md: a table of what is vendored and why, so the answer to "does X work
  on this port" is visible without reading CLAUDE.md.
- TOOLING.md: audit-exclusions.py alongside the other two guardrails, the
  ecosystem refs in the version map, and the ref-bump runbook generalised off
  Compose-only.
- bridge README: the ecosystem coordinates a consumer can declare, plus the two
  caveats (nav2 is a dead end; pick one lifecycle coordinate set).
- PLAN.md 7b: marked done, with the three findings worth remembering.
2026-09-12 00:39:04 +02:00
Bitsy b965c505ee feat(pulse): vendor the Pulse MVI stack for Kotlin/Native desktop
bitsycore/pulse-mvi targets android / jvm / ios / js / wasm and publishes NO
desktop-native artifact at all, so all four modules are vendored:

  :pulse:pulse             the MVI core (store, state, intents)
  :pulse:pulse-viewmodel   ViewModel binding
  :pulse:pulse-savedstate  SavedStateHandle persistence
  :pulse:pulse-compose     Compose bindings

Vendored VERBATIM @ 0.3.7 (new PULSE_REF pin + manifests); 14 files, commonMain
only, zero local edits. Pinned to the TAG, not main: the three commits past
0.3.7 are docs and CI only, and a tag is the durable ref this repo's vendoring
rules ask for.

GOOGLE lifecycle coordinates again, not upstream's org.jetbrains.androidx.*
mirrors - same duplicate-classes reason as the Koin modules.

COORDINATES: upstream publishes com.bitsycore.lib:*, which this repo must not
republish under the same name. The port publishes com.bitsycore.pulse:* instead
and the bridge substitutes com.bitsycore.lib:* -> the project modules on native
configurations, so consumers keep declaring the upstream coordinate.
2026-09-12 00:35:30 +02:00
Bitsy 05faaa7f7c feat(coil): vendor the Coil 3 stack for Kotlin/Native desktop
Coil 3.6.2 publishes linux + apple klibs for its core/network/svg modules but
NO mingwX64 anywhere, and NO desktop-native artifact at all for the compose
layer. The sources are target-agnostic, so the port re-declares the targets and
rebuilds them:

  :coil:coil-core            coil3.* core (Skia-based decoding)
  :coil:coil                 the singleton ImageLoader facade
  :coil:coil-compose-core    AsyncImage / rememberAsyncImagePainter internals
  :coil:coil-compose         the public compose entry points
  :coil:coil-svg             SVG decoding via skiko SVGDOM
  :coil:coil-network-core    shared network-fetcher plumbing
  :coil:coil-network-ktor3   Ktor 3 fetcher, on the port's existing ktor pin

Vendored VERBATIM from coil-kt/coil @ 3.6.2 (new COIL_REF pin + manifests);
152 files, zero local edits - all of it compiled unmodified.

SOURCE-SET COLLAPSE: the port has no android, jvm or js target, so upstream's
nonAndroidMain / nonJvmCommonMain / nonJsCommonMain all apply to every target
here. They do NOT merge into nativeMain though: several declare `expect`s whose
`actual`s live in upstream nativeMain, and siblings in one source set is a hard
error. So they keep their own level above it.

TWO PARALLEL SKIKO BRANCHES in coil-core / coil-svg / coil-compose-core:
upstream's non-android code uses org.jetbrains.skia.* directly, so the
INTERMEDIATE source set holding it needs skiko on its own compile classpath (a
shared-metadata compilation is a real compile). macOS/Linux take the official
skiko and mingwX64 takes the bitsycore fork, and the two cannot share an
intermediate - so mingw gets a parallel branch over the same srcDirs, the same
shape :compose:ui:ui-graphics already uses. Kotlin emits no metadata
compilation for the single-target fork branch, so the two never collide.

coil-compose-core declares the whole ui / foundation family DIRECTLY rather
than leaning on foundation's transitives - the granular-metadata trap from
CLAUDE.md's "Common pitfalls": a substituted coord's transitives drop off the
commonMain metadata classpath.

Published as com.bitsycore.coil3:<module>; bridge substitution rules added so
consumers keep declaring the official io.coil-kt.coil3 coordinates.
2026-09-12 00:30:01 +02:00
Bitsy 59bb159351 feat(koin): vendor the Koin modules that stop at apple+android upstream
koin-core 4.2.2 already publishes mingwX64 + linux klibs, so it stays a plain
Maven dependency. These four do not, and their sources are platform-agnostic,
so the port rebuilds the same code for its desktop targets:

  :koin:koin-core-viewmodel      org.koin.viewmodel.* + module DSL
  :koin:koin-compose             org.koin.compose.*
  :koin:koin-compose-viewmodel   org.koin.compose.viewmodel.*
  :koin:koin-compose-navigation3 org.koin.compose.navigation3.*

Vendored VERBATIM from InsertKoinIO/koin @ 4.2.2 (new KOIN_REF pin + manifests);
36 files, no local edits needed - everything compiled for mingwX64 unmodified.

NAV3, NOT NAV2: the requested koin-compose-viewmodel-navigation sits on nav2's
navigation-compose, which publishes no mingwX64/linux klibs under EITHER the
androidx.* or org.jetbrains.androidx.* coordinates - it cannot reach this port's
targets without vendoring nav2 too. This port's navigation story is Navigation 3
(:navigation3:navigation3-ui), and koin-compose-navigation3 needs only
koin-compose + navigation3-runtime, both already reachable. Vendored that instead.

GOOGLE lifecycle coordinates (androidx.lifecycle:*), not the
org.jetbrains.androidx.* mirrors upstream koin uses: the port already resolves
the google ones, and mixing both puts the same classes on the path twice.

Published as com.bitsycore.koin:<module>; bridge substitution rules added so
consumers keep declaring the official io.insert-koin coordinates.
2026-09-12 00:19:39 +02:00
Bitsy fcba77b622 docs(plan): record the JVM default-font parity defect + the Koin/Coil vendoring audit
7a: the apidemo SourceTag pill divergence is a font-resolution difference, not a
centering bug - native FontFamily.Default is the bundled NotoSans (SkiaFonts.
defaultTypeface), the JVM leg never registers it so Default is the system font.
Every un-familied Text is being compared against a different typeface.

7b: target-availability table for the requested libraries. navigationevent-compose
needs NO vendoring (both coordinate sets ship mingwX64 + linux + macos, and the
port already consumes it); Koin needs 4 modules, Coil3 needs 7.
2026-09-12 00:05:18 +02:00
Bitsy 82ffd7cf4d feat: migrate to Compose Multiplatform 1.12.0 + SDL3 3.4.16
Upstream baseline
- Pin both vendored refs to the STABLE v1.12.0 tag (core + umbrella); the
  native and JVM-parity legs are back in lockstep, no dev-build skew.
- Catalog: compose 1.12.0, composeRuntime 1.12.0. material3 stays
  1.12.0-alpha03 - that IS the version CMP 1.12.0 ships (its own train).
- Re-synced 1560 vendored files; drift + vendor-clean both clean.
- Retire the SelectionLayout.kt manual vendor: upstream fixed the same
  out-of-bounds at the source (lastSlot = currentSlot, not currentSlot + 1),
  so the fork's index clamps are redundant. Back to verbatim vendoring.
- Re-stamp 10 manual vendors to @ v1.12.0 (upstream unchanged base..pin).
- Regenerate the klib API dumps. These had already drifted on main (the last
  dump predates the scrollbar / IME changes), so the diff covers that too.

SDL3 3.4.16, skiko fork 0.150.1-mingw.2
- Rebuild the static libSDL3.a from release-3.4.16.
- CMP 1.12.0 uses skiko 0.150.1, unchanged, so the fork's Skia base still
  matches; bump to the -mingw.2 build and centralise it as `skikoMingw` in
  the version catalog instead of three hardcoded copies.
- Resolve the fork from the PUBLIC maven.bitsycore.com/releases instead of
  the credentialed GitHub Packages repo: building the Windows target no
  longer needs a GitHub token.

Gradle module paths mirror the directory
- Drop every projectDir redirect: :ui -> :compose:ui:ui,
  :animation-core -> :compose:animation:animation-core, and so on.
- INVARIANT (documented in settings.gradle.kts + CLAUDE.md): the leaf
  directory IS the published artifactId. KMP derives a target publication's
  coordinate as <project.name>-<target>, and a target disabled on the
  building host (all Apple targets on Windows) has no publication object to
  retarget - while the root kotlinMultiplatform metadata, which the WINDOWS
  publish job owns, still emits an available-at for it. A mismatched leaf
  silently ships a root module pointing at a nonexistent <leaf>-macosarm64.
  So compose/desktop/native/window and components/resources/library are
  renamed to their artifactIds. Published coordinates are unchanged.

Manual-vendor audit
- New scripts/compose-fork/audit-exclusions.py: classifies every `!` manifest
  exclusion as manual vendor (VENDOR-BASE, drift-tracked), reimplementation
  (VENDOR-REIMPL, deliberately not tracked), or orphan, and fails on any
  unclassified one.
- 14 project reimplementations were invisible to the drift checker by
  accident; mark them VENDOR-REIMPL. None needed reconciling (all verified
  <= 0.53 similarity to their upstream counterparts, none changed upstream
  between the old pin and v1.12.0).

Fix the JVM apidemo build
- Declare components-resources on jvmMain (decodeToImageBitmap /
  decodeToSvgPainter) and add the two missing cross-package imports. The JVM
  parity target had never compiled.

Verified on Windows: both native executables link and render, apidemo JVM
builds and launches, apiCheck green, common metadata compiles (the Windows
publish gate), publishToMavenLocal emits the same coordinates as before.
2026-09-12 00:03:13 +02:00
Bitsy d3eef74803 build: update Gradle wrapper to version 9.7.1 and add toolchain configuration 2026-09-11 23:29:20 +02:00
Bitsy a07f4634b2 chore: add local.properties to .gitignore 2026-09-11 23:21:23 +02:00
Bitsy 164d2b5e66 style: replace em dashes with plain punctuation 2026-08-09 21:45:57 +02:00
390 changed files with 10713 additions and 4763 deletions
+17 -17
View File
@@ -23,7 +23,7 @@ jobs:
# Each host runs only the publish tasks Gradle actually generated for it. # Each host runs only the publish tasks Gradle actually generated for it.
# WINDOWS also publishes the shared kotlinMultiplatform metadata modules # WINDOWS also publishes the shared kotlinMultiplatform metadata modules
# (see its matrix entry) + the jvm jar + the bridge plugin. # (see its matrix entry) + the jvm jar + the bridge plugin.
# `target` is the Gradle K/N target token (Capitalised) — one per host, # `target` is the Gradle K/N target token (Capitalised) - one per host,
# used for :demo / :apidemo linkReleaseExecutable<Target>. linuxArm64 is # used for :demo / :apidemo linkReleaseExecutable<Target>. linuxArm64 is
# skipped for the app build because SDL3 static libs on the ubuntu runner # skipped for the app build because SDL3 static libs on the ubuntu runner
# are host-arch only; cross-compile would fail at link time. # are host-arch only; cross-compile would fail at link time.
@@ -49,7 +49,7 @@ jobs:
# WINDOWS owns the root KotlinMultiplatform metadata modules: it is # WINDOWS owns the root KotlinMultiplatform metadata modules: it is
# the ONLY host that declares every target (vHostSupportsMingw is # the ONLY host that declares every target (vHostSupportsMingw is
# host-gated), so only its generated .module files carry the full # host-gated), so only its generated .module files carry the full
# variant table — the macOS-published roots of v0.1.15 had NO # variant table - the macOS-published roots of v0.1.15 had NO
# mingwX64 variants and Windows consumers could not resolve them. # mingwX64 variants and Windows consumers could not resolve them.
# The host-independent jvm jar (:material-symbols) and the bridge # The host-independent jvm jar (:material-symbols) and the bridge
# plugin ride along on the same job. # plugin ride along on the same job.
@@ -102,13 +102,13 @@ jobs:
# windows-2022 runners pre-install Chocolatey's mingw-w64 at C:\mingw64. # windows-2022 runners pre-install Chocolatey's mingw-w64 at C:\mingw64.
# SDL3 >= 3.4 needs a C++ compiler for its GameInput backend (K/N's # SDL3 >= 3.4 needs a C++ compiler for its GameInput backend (K/N's
# bundled mingw is C-only) — this pre-install covers that. The setjmp # bundled mingw is C-only) - this pre-install covers that. The setjmp
# intrinsic K/N's LLD can't resolve is aliased to msvcrt's _setjmpex at # intrinsic K/N's LLD can't resolve is aliased to msvcrt's _setjmpex at
# link time via --defsym in sdl3.def. # link time via --defsym in sdl3.def.
# Build the static SDL3 lib (scripts/build-sdl/build-sdl.properties). Every # Build the static SDL3 lib (scripts/build-sdl/build-sdl.properties). Every
# target renders through Skia now, so all platforms need only base SDL3 for # target renders through Skia now, so all platforms need only base SDL3 for
# the main loop (windowing / input / audio / filesystem) — the from-scratch # the main loop (windowing / input / audio / filesystem) - the from-scratch
# SDL renderer and its SDL3_ttf / SDL3_image / FreeType deps were removed. # SDL renderer and its SDL3_ttf / SDL3_image / FreeType deps were removed.
# Cached against the manifest + scripts so version bumps invalidate cleanly; # Cached against the manifest + scripts so version bumps invalidate cleanly;
# bump the `v3` key when the toolchain (runner OS, compiler flavour, K/N # bump the `v3` key when the toolchain (runner OS, compiler flavour, K/N
@@ -125,7 +125,7 @@ jobs:
shell: bash shell: bash
run: python3 scripts/build-sdl/build-all.py run: python3 scripts/build-sdl/build-all.py
# src/vendor/ trees are gitignored — a fresh checkout has zero vendored # src/vendor/ trees are gitignored - a fresh checkout has zero vendored
# androidx.compose.* files. Sync copies them in from the upstream refs # androidx.compose.* files. Sync copies them in from the upstream refs
# pinned in scripts/compose-fork/compose.properties. Needs python3 + git; # pinned in scripts/compose-fork/compose.properties. Needs python3 + git;
# both are pre-installed on every runner. # both are pre-installed on every runner.
@@ -138,7 +138,7 @@ jobs:
# SEQUENTIALLY from the first one. So when an upload dies mid-publication # SEQUENTIALLY from the first one. So when an upload dies mid-publication
# (the v0.1.13 macOS run lost DNS mid-publish), a blind retry hits 409 # (the v0.1.13 macOS run lost DNS mid-publish), a blind retry hits 409
# Conflict on the first already-uploaded file and aborts BEFORE reaching # Conflict on the first already-uploaded file and aborts BEFORE reaching
# the missing tail files — the version is poisoned and can never # the missing tail files - the version is poisoned and can never
# complete. A 409 is therefore only benign when the publication had # complete. A 409 is therefore only benign when the publication had
# already completed; you can't tell from the 409 alone. The fix: between # already completed; you can't tell from the 409 alone. The fix: between
# attempts, DELETE this host's package versions for the current release # attempts, DELETE this host's package versions for the current release
@@ -160,7 +160,7 @@ jobs:
linux) SUFFIXES="-linuxx64 -linuxarm64" ;; linux) SUFFIXES="-linuxx64 -linuxarm64" ;;
windows) SUFFIXES="_ROOT_ -mingwx64" ;; windows) SUFFIXES="_ROOT_ -mingwx64" ;;
esac esac
# Published group per module — mirrors the per-area com.bitsycore groups # Published group per module - mirrors the per-area com.bitsycore groups
# in root build.gradle.kts. Project-only modules (sdl-core, material-symbols) # in root build.gradle.kts. Project-only modules (sdl-core, material-symbols)
# fall through to com.bitsycore.compose.sdl; the GitHub Packages Maven # fall through to com.bitsycore.compose.sdl; the GitHub Packages Maven
# package name is "<group>.<artifact>". # package name is "<group>.<artifact>".
@@ -202,7 +202,7 @@ jobs:
-H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/users/$OWNER/packages/maven/$pkg/versions/$vid") "https://api.github.com/users/$OWNER/packages/maven/$pkg/versions/$vid")
if [ "$code" != "204" ]; then if [ "$code" != "204" ]; then
# Deleting the LAST version of a package is refused (400) — # Deleting the LAST version of a package is refused (400) -
# a package new in this release: drop the whole package. # a package new in this release: drop the whole package.
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Authorization: Bearer $GITHUB_TOKEN" \
@@ -219,7 +219,7 @@ jobs:
fi fi
echo "::endgroup::" echo "::endgroup::"
if [ $attempt -lt 3 ]; then if [ $attempt -lt 3 ]; then
echo "::warning::Publish attempt $attempt failed — cleaning this host's $VER packages and retrying" echo "::warning::Publish attempt $attempt failed - cleaning this host's $VER packages and retrying"
cleanup_host_versions || true cleanup_host_versions || true
sleep 30 sleep 30
fi fi
@@ -247,19 +247,19 @@ jobs:
fi fi
echo "::endgroup::" echo "::endgroup::"
if [ $attempt -lt 3 ]; then if [ $attempt -lt 3 ]; then
echo "::warning::Bitsycore publish attempt $attempt failed — retrying" echo "::warning::Bitsycore publish attempt $attempt failed - retrying"
sleep 30 sleep 30
fi fi
done done
echo "::error::Bitsycore publish failed after 3 attempts" echo "::error::Bitsycore publish failed after 3 attempts"
exit 1 exit 1
# Release binaries for the host target — the binary plus the data.kres # Release binaries for the host target - the binary plus the data.kres
# (bundled composeResources + fonts + subset icon-font). Each app's # (bundled composeResources + fonts + subset icon-font). Each app's
# linkReleaseExecutable<Target> auto-runs its data.kres zip task — # linkReleaseExecutable<Target> auto-runs its data.kres zip task -
# :demo's own copy<Variant>ComposeResources<Target>, :apidemo's # :demo's own copy<Variant>ComposeResources<Target>, :apidemo's
# bridge-plugin package<Variant>ComposeResources<Target> — so data.kres # bridge-plugin package<Variant>ComposeResources<Target> - so data.kres
# is produced next to the .kexe / .exe. On Linux only linuxX64 is built — # is produced next to the .kexe / .exe. On Linux only linuxX64 is built -
# linuxArm64 is skipped since the SDL3 static libs on the ubuntu runner # linuxArm64 is skipped since the SDL3 static libs on the ubuntu runner
# are host-arch (x64). # are host-arch (x64).
- name: Build demo + apidemo release binaries - name: Build demo + apidemo release binaries
@@ -274,7 +274,7 @@ jobs:
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
# Lower-case first letter — gradle target tokens are Capitalised but # Lower-case first letter - gradle target tokens are Capitalised but
# the build/bin dir uses the plain target name (macosArm64 etc). # the build/bin dir uses the plain target name (macosArm64 etc).
target_dir=$(echo "${{ matrix.target }}" | awk '{print tolower(substr($0,1,1)) substr($0,2)}') target_dir=$(echo "${{ matrix.target }}" | awk '{print tolower(substr($0,1,1)) substr($0,2)}')
version="${GITHUB_REF_NAME#v}" version="${GITHUB_REF_NAME#v}"
@@ -283,8 +283,8 @@ jobs:
src="$app/build/bin/$target_dir/releaseExecutable" src="$app/build/bin/$target_dir/releaseExecutable"
zip_path="$(pwd)/dist/$app-$version-$target_dir.zip" zip_path="$(pwd)/dist/$app-$version-$target_dir.zip"
files=("$src/$app.${{ matrix.exe_ext }}" "$src/data.kres") files=("$src/$app.${{ matrix.exe_ext }}" "$src/data.kres")
# Windows Skia leg ships two runtime sidecars next to the exe — the # Windows Skia leg ships two runtime sidecars next to the exe - the
# skiko fork DLL and Skia's ICU data (icudtl.dat) — provisioned by the # skiko fork DLL and Skia's ICU data (icudtl.dat) - provisioned by the
# bridge plugin after linkReleaseExecutableMingwX64. The exe cannot run # bridge plugin after linkReleaseExecutableMingwX64. The exe cannot run
# without them, so include them (and fail loudly if a provision regressed). # without them, so include them (and fail loudly if a provision regressed).
if [ "${{ matrix.exe_ext }}" = "exe" ]; then if [ "${{ matrix.exe_ext }}" = "exe" ]; then
+2 -1
View File
@@ -53,7 +53,7 @@ bin/
### Vendored Compose sources ### ### Vendored Compose sources ###
# Byte-for-byte verbatim copies from JetBrains/compose-multiplatform-core, # Byte-for-byte verbatim copies from JetBrains/compose-multiplatform-core,
# regenerated on demand by scripts/compose-fork/sync.sh. Not committed — only the # regenerated on demand by scripts/compose-fork/sync.sh. Not committed - only the
# sync tooling (script + <module>/compose-fork.txt manifests + pinned ref + README) # sync tooling (script + <module>/compose-fork.txt manifests + pinned ref + README)
# is tracked. # is tracked.
**/src/vendor/ **/src/vendor/
@@ -64,3 +64,4 @@ __pycache__/
# Probe artifacts written to the repo root by demo --nav3test / --searchesctest / --dialoganimtest # Probe artifacts written to the repo root by demo --nav3test / --searchesctest / --dialoganimtest
/*.bmp /*.bmp
/local.properties
+309 -144
View File
@@ -1,45 +1,45 @@
# CLAUDE.md # CLAUDE.md
Guidance for Claude Code working in this repository. This is the primary Guidance for Claude Code working in this repository. This is the primary
context — read it first, then look at the files it points to. context - read it first, then look at the files it points to.
## Documentation map ## Documentation map
- [README.md](README.md) — public overview + quickstart (bridge plugin, - [README.md](README.md) - public overview + quickstart (bridge plugin,
`nativeComposeWindow`, sample apps). `nativeComposeWindow`, sample apps).
- Renderer — one Skia `RenderBackend` (Metal on macOS / OpenGL on Linux+Windows / - Renderer - one Skia `RenderBackend` (Metal on macOS / OpenGL on Linux+Windows /
CPU-raster fallback) driving upstream's vendored GraphicsLayer + Canvas engine. CPU-raster fallback) driving upstream's vendored GraphicsLayer + Canvas engine.
Retained per-node display lists: transform / alpha / clip changes REPLAY without Retained per-node display lists: transform / alpha / clip changes REPLAY without
re-recording (only a content change or resize re-records; dirty-region rendering re-recording (only a content change or resize re-records; dirty-region rendering
is a non-goal). Text is a reduced-local port over Skia `skparagraph`. Read these is a non-goal). Text is a reduced-local port over Skia `skparagraph`. Read these
before touching renderer / graphics-actual / layer-engine code: before touching renderer / graphics-actual / layer-engine code:
`SkiaRenderBackend.kt`, `RenderBackend.kt`, `GpuMode.kt`, `ComposeRootHost.kt` `SkiaRenderBackend.kt`, `RenderBackend.kt`, `GpuMode.kt`, `ComposeRootHost.kt`
(root host + snapshot-observation sweep — omitting `clearInvalidObservations()` (root host + snapshot-observation sweep - omitting `clearInvalidObservations()`
once leaked the whole graph), `ComposeOwner.kt`, and `SkiaParagraph.native.kt` / once leaked the whole graph), `ComposeOwner.kt`, and `SkiaParagraph.native.kt` /
`SkiaParagraphEngine.kt`. Open renderer/fidelity work lives in [PLAN.md](PLAN.md). `SkiaParagraphEngine.kt`. Open renderer/fidelity work lives in [PLAN.md](PLAN.md).
- [TOOLING.md](TOOLING.md) — build/vendor/verify scripts and workflows - [TOOLING.md](TOOLING.md) - build/vendor/verify scripts and workflows
(build-sdl, sync + drift checks, parity, probe, profiler, coverage, (build-sdl, sync + drift checks, parity, probe, profiler, coverage,
verify-mac) + the version map and the ref-bump / release runbooks. verify-mac) + the version map and the ref-bump / release runbooks.
- [PLAN.md](PLAN.md) §2 — audited list of no-ops, stubs, and hardcodes left in the - [PLAN.md](PLAN.md) §2 - audited list of no-ops, stubs, and hardcodes left in the
port, with P0/P1/P2 severity, plus the road-to-1.0.0 fidelity / SDL / release work port, with P0/P1/P2 severity, plus the road-to-1.0.0 fidelity / SDL / release work
(this subsumes the former TODO.md, which was never committed). (this subsumes the former TODO.md, which was never committed).
- This file — architecture, module layout, vendoring rules, source-set - This file - architecture, module layout, vendoring rules, source-set
hierarchy, density flow, conventions, and common pitfalls. hierarchy, density flow, conventions, and common pitfalls.
## What this project is ## What this project is
**ComposeNativeSDL3** — a Kotlin/Native port of Compose Multiplatform running **ComposeNativeSDL3** - a Kotlin/Native port of Compose Multiplatform running
on SDL3, no JVM. Compiles to native binaries for macOS (arm64), Linux on SDL3, no JVM. Compiles to native binaries for macOS (arm64), Linux
(x64/arm64), Windows (mingwX64). (x64/arm64), Windows (mingwX64).
Rendering is **Skia everywhere** behind one `RenderBackend` — Metal / OpenGL Rendering is **Skia everywhere** behind one `RenderBackend` - Metal / OpenGL
/ CPU raster: / CPU raster:
- **macOS + Linux** link the OFFICIAL Skiko klibs from Maven. - **macOS + Linux** link the OFFICIAL Skiko klibs from Maven.
- **Windows (mingwX64)** links the bitsycore skiko FORK — skiko+Skia compiled - **Windows (mingwX64)** links the bitsycore skiko FORK - skiko+Skia compiled
into `skiko-windows-x64.dll` with a flat extern-C surface, bound from K/N via into `skiko-windows-x64.dll` with a flat extern-C surface, bound from K/N via
an embedded GNU import lib, published to GitHub Packages as an embedded GNU import lib, published to https://maven.bitsycore.com/releases as
`com.bitsycore.skiko:skiko:0.150.1-mingw.1` (override with `com.bitsycore.skiko:skiko:0.150.1-mingw.2` (override with
`-PskikoMingwVersion`). The runtime DLL is auto-provisioned next to the exe by `-PskikoMingwVersion`). The runtime DLL is auto-provisioned next to the exe by
the bridge plugin (`installWindowsSkiaDll`). the bridge plugin (`installWindowsSkiaDll`).
@@ -47,7 +47,7 @@ Windowing, input, audio, filesystem access, and the OS-integration surface
(file dialogs, clipboard, "open in Finder/Explorer"…) all go through (file dialogs, clipboard, "open in Finder/Explorer"…) all go through
**SDL3**. The runtime (`androidx.compose.runtime.*`: composition, snapshots, **SDL3**. The runtime (`androidx.compose.runtime.*`: composition, snapshots,
recomposer, `mutableStateOf`, `remember`, …) is the **official recomposer, `mutableStateOf`, `remember`, …) is the **official
`org.jetbrains.compose.runtime` klibs from Maven** — this project only `org.jetbrains.compose.runtime` klibs from Maven** - this project only
re-implements the layers on top (`androidx.compose.ui.*`, `.foundation.*`, re-implements the layers on top (`androidx.compose.ui.*`, `.foundation.*`,
`.animation.*`, `.material3.*`). `.animation.*`, `.material3.*`).
@@ -55,67 +55,91 @@ re-implements the layers on top (`androidx.compose.ui.*`, `.foundation.*`,
Library modules mirror upstream Compose Multiplatform's `compose/` tree. Library modules mirror upstream Compose Multiplatform's `compose/` tree.
The SDL layer is two modules: `:sdl-core` (the NAKED sdl3 cinterop + platform The SDL layer is two modules: `:sdl-core` (the NAKED sdl3 cinterop + platform
primitives — zero Compose dep, like `skiko`) at `sdl/sdl-core/`, and primitives - zero Compose dep, like `skiko`) at `sdl/sdl-core/`, and
`:desktop-native-window` (the SDL3 main-loop shell + app entry point) at `:desktop-native-window` (the SDL3 main-loop shell + app entry point) at
`compose/desktop/native/window/`. `:ui` depends on `:sdl-core` and its renderer `compose/desktop/native/desktop-native-window/`. `:ui` depends on `:sdl-core` and its renderer
+ SDL↔Compose bridges pick the cinterop from it. + SDL↔Compose bridges pick the cinterop from it.
One Gradle module per upstream artifact; the directory mirrors the upstream One Gradle module per upstream artifact; the directory mirrors the upstream
`compose/` path, the gradle path is kept short (redirected via `projectDir`). `compose/` path, and the gradle path mirrors the directory (no `projectDir`
redirection).
``` ```
compose/ compose/
├── ui/ ├── ui/
│ ├── ui/ → :ui — androidx.compose.ui.* CORE (Modifier, LayoutNode, │ ├── ui/ → :compose:ui:ui - androidx.compose.ui.* CORE (Modifier, LayoutNode,
│ │ composition, semantics, input, focus) + com.compose.sdl.* — │ │ composition, semantics, input, focus) + com.compose.sdl.* -
│ │ the Skia RenderBackend + GPU bridges + the SDL↔Compose │ │ the Skia RenderBackend + GPU bridges + the SDL↔Compose
│ │ bridges (events / clipboard / cursors / window). Depends on │ │ bridges (events / clipboard / cursors / window). Depends on
│ │ :ui-graphics, :ui-text, :sdl-core. (ui-graphics + ui-text + │ │ :ui-graphics, :ui-text, :sdl-core. (ui-graphics + ui-text +
│ │ the sdl3 cinterop were split OUT — upstream layout.) │ │ the sdl3 cinterop were split OUT - upstream layout.)
│ ├── ui-graphics/ → :ui-graphics — androidx.compose.ui.graphics.* + the Skia actuals │ ├── ui-graphics/ → :compose:ui:ui-graphics - androidx.compose.ui.graphics.* + the Skia actuals
│ │ (SkiaBackedCanvas/Path/Paint, GraphicsLayer, SkiaImageCache, │ │ (SkiaBackedCanvas/Path/Paint, GraphicsLayer, SkiaImageCache,
│ │ painter/image + resource seams). → skiko; SDL-free. │ │ painter/image + resource seams). → skiko; SDL-free.
│ ├── ui-text/ → :ui-text — androidx.compose.ui.text.* + the skiko text engine │ ├── ui-text/ → :compose:ui:ui-text - androidx.compose.ui.text.* + the skiko text engine
│ │ (SkiaParagraph → NativeParagraphOps → SkiaFonts, IconFont, │ │ (SkiaParagraph → NativeParagraphOps → SkiaFonts, IconFont,
│ │ NamedFont). → :ui-graphics, skiko; SDL-free. │ │ NamedFont). → :ui-graphics, skiko; SDL-free.
│ ├── ui-util/ → :ui-util — androidx.compose.ui.util.* (+ Experimental/InternalComposeUiApi) │ ├── ui-util/ → :compose:ui:ui-util - androidx.compose.ui.util.* (+ Experimental/InternalComposeUiApi)
│ ├── ui-geometry/ → :ui-geometry — androidx.compose.ui.geometry.* │ ├── ui-geometry/ → :compose:ui:ui-geometry - androidx.compose.ui.geometry.*
│ ├── ui-unit/ → :ui-unit — androidx.compose.ui.unit.* │ ├── ui-unit/ → :compose:ui:ui-unit - androidx.compose.ui.unit.*
│ ├── ui-backhandler/ → :ui-backhandler — androidx.compose.ui.backhandler.* │ ├── ui-backhandler/ → :compose:ui:ui-backhandler - androidx.compose.ui.backhandler.*
│ └── ui-tooling-preview/ → :ui-tooling-preview — androidx.compose.ui.tooling.preview.* │ └── ui-tooling-preview/ → :compose:ui:ui-tooling-preview - androidx.compose.ui.tooling.preview.*
│ (the common @Preview + PreviewParameterProvider, │ (the common @Preview + PreviewParameterProvider,
│ vendored verbatim; the Maven artifact ships no │ vendored verbatim; the Maven artifact ships no
│ mingwX64/linux klibs). IDE-only metadata — previews │ mingwX64/linux klibs). IDE-only metadata - previews
│ render through the apps' jvm parity targets. │ render through the apps' jvm parity targets.
├── animation/ ├── animation/
│ ├── animation-core/ → :animation-core — androidx.compose.animation.core.* │ ├── animation-core/ → :compose:animation:animation-core - androidx.compose.animation.core.*
│ ├── animation/ → :animation — androidx.compose.animation.* (non-core) │ ├── animation/ → :compose:animation:animation - androidx.compose.animation.* (non-core)
│ └── animation-graphics/ → :animation-graphics — androidx.compose.animation.graphics.* │ └── animation-graphics/ → :compose:animation:animation-graphics - androidx.compose.animation.graphics.*
├── foundation/ ├── foundation/
│ ├── foundation/ → :foundation — androidx.compose.foundation.* │ ├── foundation/ → :compose:foundation:foundation - androidx.compose.foundation.*
│ └── foundation-layout/ → :foundation-layout — androidx.compose.foundation.layout.* │ └── foundation-layout/ → :compose:foundation:foundation-layout - androidx.compose.foundation.layout.*
├── material3/ ├── material3/
│ └── material3/ → :material3 — androidx.compose.material3.* │ ├── material3/ → :compose:material3:material3 - androidx.compose.material3.*
│ └── adaptive/ → the material3-adaptive family, VENDORED VERBATIM.
│ │ org.jetbrains.compose.material3.adaptive:* publishes
│ │ ios + macosArm64 ONLY - no linux, no mingw (issue #4).
│ │ Nothing in the sources blocks those targets: every
│ │ actual the native legs need already exists upstream
│ │ (nonAndroidMain / skikoMain / nativeMain), and the one
│ │ real dependency, androidx.window:window-core, ALREADY
│ │ publishes linux + mingw + macos. The artifacts were
│ │ missing purely because upstream never enabled the
│ │ targets - so this port is vendor-only, ZERO source
│ │ edits. Upstream's skikoMain (the ~75 l10n tables +
│ │ Strings / drag-handle actuals) lands in src/vendor/
│ │ native/ next to upstream's own nativeMain actuals;
│ │ they actualise disjoint expects so they coexist.
│ │ com.bitsycore.compose.material3.adaptive:<module>.
│ ├── adaptive/ → :compose:material3:adaptive:adaptive - WindowAdaptiveInfo /
│ │ Posture / window size class. api-exposes window-core.
│ ├── adaptive-layout/ → :compose:material3:adaptive:adaptive-layout - ListDetail /
│ │ SupportingPane / ThreePane scaffolds + pane motion
│ ├── adaptive-navigation/ → :compose:material3:adaptive:adaptive-navigation -
│ │ ThreePaneScaffoldNavigator + back behaviour
│ └── adaptive-navigation3/ → :compose:material3:adaptive:adaptive-navigation3 -
│ SceneStrategy bindings for nav3 (→ :navigation3-ui)
├── material/ ├── material/
│ └── material-ripple/ → :material-ripple — androidx.compose.material.ripple.* │ └── material-ripple/ → :compose:material:material-ripple - androidx.compose.material.ripple.*
└── desktop/native/window/ → :desktop-native-window — nativeComposeApp { Window(...) {} } └── desktop/native/desktop-native-window/ → :compose:desktop:native:desktop-native-window - nativeComposeApp { Window(...) {} }
multi-window shell + SDL3 main loop; nativeComposeWindow() multi-window shell + SDL3 main loop; nativeComposeWindow()
wrapper (project app-shell, not an upstream CMP artifact). wrapper (project app-shell, not an upstream CMP artifact).
Published as com.bitsycore.compose:desktop-native-window. Published as com.bitsycore.compose:desktop-native-window.
sdl/ sdl/
└── sdl-core/ → :sdl-core — the NAKED sdl3 cinterop + platform primitives └── sdl-core/ → :sdl:sdl-core - the NAKED sdl3 cinterop + platform primitives
(zero Compose dep, like skiko); the single `sdl3` cinterop (zero Compose dep, like skiko); the single `sdl3` cinterop
lives here. :ui depends on it. com.bitsycore.compose.sdl:sdl-core. lives here. :ui depends on it. com.bitsycore.compose.sdl:sdl-core.
utils/ utils/
└── material-symbols/ → :material-symbols — codepoints + all three style objects └── material-symbols/ → :utils:material-symbols - codepoints + all three style objects
(Outlined / Rounded / Sharp). COMMON API (usable from (Outlined / Rounded / Sharp). COMMON API (usable from
shared app code) + per-stack actuals: native renders shared app code) + per-stack actuals: native renders
via :foundation IconFontIcon (Skia), jvm() via Skiko directly via :foundation IconFontIcon (Skia), jvm() via Skiko directly
(Typeface.makeClone per axes — upstream's FontCache (Typeface.makeClone per axes - upstream's FontCache
drops variationSettings from its key). Its commonMain drops variationSettings from its key). Its commonMain
declares official Maven compose coords — the root declares official Maven compose coords - the root
build's FULL-COMMONIZATION BRIDGE substitutes the build's FULL-COMMONIZATION BRIDGE substitutes the
whole ui / foundation / animation / material3 / whole ui / foundation / animation / material3 /
nav3-ui / components-resources family to project nav3-ui / components-resources family to project
@@ -127,7 +151,7 @@ utils/
jvmProcessResources stages the same fonts (jvm). jvmProcessResources stages the same fonts (jvm).
components/ components/
└── resources/library/ → :components-resources — the OFFICIAL Compose resources └── resources/components-resources/ → :components:resources:components-resources - the OFFICIAL Compose resources
runtime (org.jetbrains.compose.components: runtime (org.jetbrains.compose.components:
components-resources), VENDORED from the components-resources), VENDORED from the
compose-multiplatform UMBRELLA repo (the first compose-multiplatform UMBRELLA repo (the first
@@ -137,11 +161,63 @@ components/
DomXmlParser (upstream's is Darwin NSXMLParser), DomXmlParser (upstream's is Darwin NSXMLParser),
image decode via the :ui EncodedImageDecoder hook image decode via the :ui EncodedImageDecoder hook
(Skia), NamedFont registration, SDL locale/theme env. (Skia), NamedFont registration, SDL locale/theme env.
Apps' JVM targets keep the Maven artifact — the Apps' JVM targets keep the Maven artifact - the
generated Res accessors work against BOTH. generated Res accessors work against BOTH.
koin/ → the Koin DI modules upstream stops publishing for desktop native.
├── koin-core-viewmodel/ → :koin:koin-core-viewmodel - org.koin.viewmodel.* + module DSL
├── koin-compose/ → :koin:koin-compose - org.koin.compose.*
├── koin-compose-viewmodel/ → :koin:koin-compose-viewmodel - org.koin.compose.viewmodel.*
└── koin-compose-navigation3/ → :koin:koin-compose-navigation3 - org.koin.compose.navigation3.*
VENDORED verbatim from InsertKoinIO/koin
(KOIN_REF). koin-core ITSELF publishes mingwX64
+ linux and is a plain Maven dep - only these
four stop at apple+android upstream. NOTE it is
the NAV3 module, not koin-compose-viewmodel-
navigation: that one sits on nav2's
navigation-compose, which has no mingwX64/linux
klibs under EITHER coordinate set.
com.bitsycore.koin:<module>.
coil/ → Coil 3 image loading. Upstream ships linux but NO mingwX64 for
│ core/network/svg, and NO desktop native at all for the compose
│ layer. VENDORED verbatim from coil-kt/coil (COIL_REF).
├── coil-core/ → :coil:coil-core - coil3.* core, Skia-based decode
├── coil/ → :coil:coil - singleton ImageLoader facade
├── coil-compose-core/ → :coil:coil-compose-core - AsyncImage internals
├── coil-compose/ → :coil:coil-compose - public compose entry points
├── coil-svg/ → :coil:coil-svg - SVG via skiko SVGDOM
├── coil-network-core/ → :coil:coil-network-core - fetcher plumbing
└── coil-network-ktor3/ → :coil:coil-network-ktor3 - Ktor 3 fetcher
Upstream's nonAndroid / nonJvmCommon /
nonJsCommon sets all apply here (no android /
jvm / js target) but must stay ABOVE nativeMain
- they declare expects whose actuals live in
nativeMain. coil-core / coil-svg /
coil-compose-core use org.jetbrains.skia
directly, so they carry TWO PARALLEL skiko
branches (official for macOS/Linux, the
bitsycore fork for mingwX64) exactly like
:compose:ui:ui-graphics. com.bitsycore.coil3:<m>.
pulse/ → Pulse MVI. Upstream (bitsycore/pulse-mvi, PULSE_REF) targets
│ android / jvm / ios / js / wasm and publishes NO desktop-native
│ artifact, so all four modules are vendored. commonMain only.
├── pulse/ → :pulse:pulse - MVI core (store/state/intents)
├── pulse-viewmodel/ → :pulse:pulse-viewmodel - ViewModel binding
├── pulse-savedstate/ → :pulse:pulse-savedstate - SavedStateHandle persistence
└── pulse-compose/ → :pulse:pulse-compose - Compose bindings
Published as com.bitsycore.compose.desktop.native
.pulse:<module>, NOT upstream's com.bitsycore.lib:
<module>. Pulse is the one vendored dep that is
ITSELF a com.bitsycore library, so the republish is
namespaced under this project rather than a bare
com.bitsycore.pulse that would read like upstream;
the bridge substitutes the upstream coord on
native configs.
navigation3/ navigation3/
└── navigation3-ui/ → :navigation3-ui — androidx.navigation3.ui.* + scene machinery, └── navigation3-ui/ → :navigation3:navigation3-ui - androidx.navigation3.ui.* + scene machinery,
VENDORED verbatim from upstream (SET_FOLDER manifest). VENDORED verbatim from upstream (SET_FOLDER manifest).
Navigation 3's runtime layers (navigation3-runtime, Navigation 3's runtime layers (navigation3-runtime,
lifecycle-viewmodel-navigation3) are real Maven KMP lifecycle-viewmodel-navigation3) are real Maven KMP
@@ -152,21 +228,21 @@ navigation3/
(700ms fades, predictive-pop spring/scaleOut) because (700ms fades, predictive-pop spring/scaleOut) because
the upstream macos actual ships all-None (no animation). the upstream macos actual ships all-None (no animation).
demo/ → :demo — flagship showcase app (30+ screens) + the CLI probe suite. demo/ → :demo - flagship showcase app (30+ screens) + the CLI probe suite.
MULTIPLATFORM: also has a jvm() target running the SAME shared MULTIPLATFORM: also has a jvm() target running the SAME shared
screens on stock JVM Compose Desktop (`./gradlew :demo:run`, screens on stock JVM Compose Desktop (`./gradlew :demo:run`,
MainJvmKt) — the parity reference; differences vs native = port bugs MainJvmKt) - the parity reference; differences vs native = port bugs
apidemo/ → :apidemo — Postman-style REST API manager. MULTIPLATFORM like :demo apidemo/ → :apidemo - Postman-style REST API manager. MULTIPLATFORM like :demo
(`./gradlew :apidemo:run`): the whole UI lives in commonMain (`./gradlew :apidemo:run`): the whole UI lives in commonMain
against the official Maven coords; SDL-backed APIs go through against the official Maven coords; SDL-backed APIs go through
expect/actual seams (compat/Compat.kt — native actuals delegate expect/actual seams (compat/Compat.kt - native actuals delegate
to com.compose.sdl, jvm actuals use AWT + upstream desktop). to com.compose.sdl, jvm actuals use AWT + upstream desktop).
mTLS / TLS-chain inspection stays native-only (bundled libcurl). mTLS / TLS-chain inspection stays native-only (bundled libcurl).
DOGFOODS the bridge plugin: data.kres packaging + app icon come DOGFOODS the bridge plugin: data.kres packaging + app icon come
from compose.desktop.native {}; the font pipeline (Noto + from compose.desktop.native {}; the font pipeline (Noto +
Material Symbols subsetting) comes from the shared buildSrc Material Symbols subsetting) comes from the shared buildSrc
helper (registerComposeFontBundling). helper (registerComposeFontBundling).
buildSrc/ → shared build logic for the app modules: ComposeFontBundling.kt — buildSrc/ → shared build logic for the app modules: ComposeFontBundling.kt -
registerComposeFontBundling { } (everything OPT-IN; no flag = no-op) registerComposeFontBundling { } (everything OPT-IN; no flag = no-op)
registers downloadNotoFonts (bundleNotoSans / bundleNotoSansMono / registers downloadNotoFonts (bundleNotoSans / bundleNotoSansMono /
autoDetectNotoSansMono), detects the Material Symbols styles the autoDetectNotoSansMono), detects the Material Symbols styles the
@@ -179,23 +255,23 @@ gradle-plugin/
└── compose-desktop-native-bridge/ → the CONSUMER-side bridge as a published Gradle plugin └── compose-desktop-native-bridge/ → the CONSUMER-side bridge as a published Gradle plugin
(id com.bitsycore.compose-desktop-native.bridge, applies to (id com.bitsycore.compose-desktop-native.bridge, applies to
Settings or Project). An INCLUDED build (pluginManagement. Settings or Project). An INCLUDED build (pluginManagement.
includeBuild in settings.gradle.kts), NOT a subproject — a includeBuild in settings.gradle.kts), NOT a subproject - a
plugins{} block can only resolve plugins from repositories or plugins{} block can only resolve plugins from repositories or
included builds; :demo and :apidemo apply it from source included builds; :demo and :apidemo apply it from source
(dogfooding: data.kres packaging — and apidemo's app icon — (dogfooding: data.kres packaging - and apidemo's app icon -
run through the plugin). run through the plugin).
Three halves: (1) substitution — third-party apps declare Three halves: (1) substitution - third-party apps declare
OFFICIAL CMP coords in commonMain and native configurations OFFICIAL CMP coords in commonMain and native configurations
swap in the published com.bitsycore.compose.sdl klibs swap in the published com.bitsycore.compose.sdl klibs
(version defaults to the plugin's own; override: (version defaults to the plugin's own; override:
composeDesktopNative.version property; disable: composeDesktopNative.version property; disable:
composeDesktopNative.substitution=false — set repo-wide in composeDesktopNative.substitution=false - set repo-wide in
gradle.properties since the root build's FULL-COMMONIZATION gradle.properties since the root build's FULL-COMMONIZATION
BRIDGE substitutes to project modules in-repo); (2) data.kres BRIDGE substitutes to project modules in-repo); (2) data.kres
packaging (package<Variant>ComposeResources<Target> per native packaging (package<Variant>ComposeResources<Target> per native
executable, honours -PcompressResources); (3) the executable, honours -PcompressResources); (3) the
compose.desktop.native { entryPoint / icon {} } DSL (.rgba compose.desktop.native { entryPoint / icon {} } DSL (.rgba
runtime icons + windres .exe embed — injected into runtime icons + windres .exe embed - injected into
hand-declared executables too). Published by the WINDOWS hand-declared executables too). Published by the WINDOWS
publish job. publish job.
scripts/ → vendor-sync + python helper scripts (compose-coverage = API scripts/ → vendor-sync + python helper scripts (compose-coverage = API
@@ -206,14 +282,37 @@ libs/ → gitignored per-host static SDL3 output of
scripts/build-sdl/build-all.py on Windows scripts/build-sdl/build-all.py on Windows
``` ```
Module PATHS stay short (`:ui`, `:foundation`, `:desktop-native-window`, …) — Module PATHS mirror the DIRECTORY 1:1 - `:compose:ui:ui`,
`settings.gradle.kts` redirects `projectDir` for each so build files across `:compose:foundation:foundation`, `:compose:desktop:native:desktop-native-window`,
the repo stay terse. `androidx.collection` is a plain Maven dependency `:sdl:sdl-core`, `:utils:material-symbols`, `:navigation3:navigation3-ui`,
(`androidx.collection:collection`), not a module — same as other simple `:components:resources:components-resources`. There is NO `projectDir` redirection in
`settings.gradle.kts`; a plain `include(...)` per module is the whole story, and
the intermediate container projects (`:compose`, `:compose:ui`, …) hold no code.
**INVARIANT: the leaf directory IS the published artifactId.** Don't break it to
mirror an upstream directory name. KMP derives every target publication's
coordinate as `<project.name>-<target>`, and a target DISABLED on the building
host (all Apple targets on Windows/Linux) has no publication object to retarget
afterwards - while the root `kotlinMultiplatform` metadata, which the WINDOWS
publish job owns, still emits an `available-at` for it. A mismatched leaf
therefore ships a root module pointing at a nonexistent `<leaf>-macosarm64`,
breaking macOS consumers, and no `artifactId = …` override can reach it. Two
directories are renamed away from upstream's layout to hold this:
`compose/desktop/native/desktop-native-window` (upstream: `window`) and
`components/resources/components-resources` (upstream: `library` + a hand-set
artifactId). `scripts/compose-coverage.py`'s `kTargets` carries the
upstream→local mapping for the latter.
`androidx.collection` is a plain Maven dependency
(`androidx.collection:collection`), not a module - same as other simple
androidx KMP libs. androidx KMP libs.
## Dependency graph ## Dependency graph
Written with LEAF names for readability (`:ui` = `:compose:ui:ui`,
`:foundation` = `:compose:foundation:foundation`, …); build files use the full
path shown in the module layout above.
``` ```
:ui ← :animation-core ← :animation ← :foundation ← :material3 ← :demo, :apidemo :ui ← :animation-core ← :animation ← :foundation ← :material3 ← :demo, :apidemo
:ui ← :foundation-layout ←──────────────────────┘ ↑ ↑ :ui ← :foundation-layout ←──────────────────────┘ ↑ ↑
@@ -224,7 +323,7 @@ androidx KMP libs.
All edges are `api`, so a consumer of `:foundation` / `:material3` transitively All edges are `api`, so a consumer of `:foundation` / `:material3` transitively
sees the split modules. Full DAG: `:ui-util → collection`; `:ui-geometry → :ui-util`; sees the split modules. Full DAG: `:ui-util → collection`; `:ui-geometry → :ui-util`;
`:ui-unit → :ui-geometry, :ui-util`; `:ui-backhandler → :ui-util, navigationevent`; `:ui-unit → :ui-geometry, :ui-util`; `:ui-backhandler → :ui-util, navigationevent`;
`:sdl-core → sdl3 cinterop (NAKED — no Compose)`; `:sdl-core → sdl3 cinterop (NAKED - no Compose)`;
`:ui-graphics → :ui-geometry, :ui-unit, :ui-util, skiko`; `:ui-graphics → :ui-geometry, :ui-unit, :ui-util, skiko`;
`:ui-text → :ui-graphics, :ui-unit, :ui-util, skiko`; `:ui-text → :ui-graphics, :ui-unit, :ui-util, skiko`;
`:ui → :ui-graphics, :ui-text, :sdl-core, :ui-util, :ui-geometry, :ui-unit, :ui-backhandler`; `:animation-core → :ui`; `:ui → :ui-graphics, :ui-text, :sdl-core, :ui-util, :ui-geometry, :ui-unit, :ui-backhandler`; `:animation-core → :ui`;
@@ -232,6 +331,14 @@ sees the split modules. Full DAG: `:ui-util → collection`; `:ui-geometry → :
`:foundation → :animation, :foundation-layout, :animation-core, :ui`; `:foundation → :animation, :foundation-layout, :animation-core, :ui`;
`:material-ripple → :foundation, :animation-core`; `:material-ripple → :foundation, :animation-core`;
`:material3 → :foundation, :material-ripple, :animation-core, :foundation-layout`. `:material3 → :foundation, :material-ripple, :animation-core, :foundation-layout`.
The adaptive family sits beside material3 rather than under it - it depends on
`:ui` / `:foundation` / `:animation-core`, NOT on `:material3`:
`:adaptive → :ui, :foundation, window-core`;
`:adaptive-layout → :adaptive, :ui, :animation-core, :animation, :foundation,
:foundation-layout, :ui-geometry, collection, window-core`;
`:adaptive-navigation → :adaptive-layout, :foundation, :ui-util`;
`:adaptive-navigation3 → :adaptive-navigation, :navigation3-ui, collection,
navigationevent-compose`.
`:ui` is the Compose core + the Skia RenderBackend + the SDL↔Compose bridges; it `:ui` is the Compose core + the Skia RenderBackend + the SDL↔Compose bridges; it
sits on `:ui-graphics` / `:ui-text` (the graphics/text primitives + their skiko sits on `:ui-graphics` / `:ui-text` (the graphics/text primitives + their skiko
@@ -241,28 +348,28 @@ Everything above `:ui` touches renderer internals only via its public surface.
`:desktop-native-window` depends on `:ui` + `:foundation` (needs `LazyList`-style scaffolding to `:desktop-native-window` depends on `:ui` + `:foundation` (needs `LazyList`-style scaffolding to
install the popup / scaffold layer at the composition root). install the popup / scaffold layer at the composition root).
## Vendoring philosophy — read this before writing any `androidx.compose.*` code ## Vendoring philosophy - read this before writing any `androidx.compose.*` code
**Prefer vendoring verbatim from upstream Compose Multiplatform over hand-rolling anything.** **Prefer vendoring verbatim from upstream Compose Multiplatform over hand-rolling anything.**
Every module that ships `androidx.compose.*` code carries a Every module that ships `androidx.compose.*` code carries a
`<module>/compose-fork.txt` manifest. Each one declares its upstream repo + `<module>/compose-fork.txt` manifest. Each one declares its upstream repo +
pinned ref up top with `SET_REPO=<https-url>@<ref>`, where `<ref>` is normally a pinned ref up top with `SET_REPO=<https-url>@<ref>`, where `<ref>` is normally a
`<VARNAME>` resolved from `scripts/compose-fork/compose.properties` — a `<VARNAME>` resolved from `scripts/compose-fork/compose.properties` - a
`NAME=value` file that version-tags every pinned ref in ONE place (e.g. `NAME=value` file that version-tags every pinned ref in ONE place (e.g.
`COMPOSE_CORE_REF` for compose-multiplatform-core, `COMPOSE_REF` for the `COMPOSE_CORE_REF` for compose-multiplatform-core, `COMPOSE_REF` for the
compose-multiplatform umbrella repo that `:components-resources` vendors from). compose-multiplatform umbrella repo that `:components-resources` vendors from).
There is no implicit default — `SET_REPO` is required. `scripts/compose-fork/sync.sh` There is no implicit default - `SET_REPO` is required. `scripts/compose-fork/sync.sh`
walks all manifests and copies each selected file byte-for-byte from the pinned walks all manifests and copies each selected file byte-for-byte from the pinned
checkout (each distinct repo sparse-cloned to `../cmp-ref[-<name>]`) into checkout (each distinct repo sparse-cloned to `../cmp-ref[-<name>]`) into
`<module>/src/vendor/{common,native,skikoRenderer}/kotlin/`. The `<module>/src/vendor/{common,native,skikoRenderer}/kotlin/`. The
`src/vendor/` tree is **gitignored** — you don't check it in, you re-sync `src/vendor/` tree is **gitignored** - you don't check it in, you re-sync
on demand. on demand.
Manifests are **folder-style**: a `SET_FOLDER=<module>/src` line sets an upstream Manifests are **folder-style**: a `SET_FOLDER=<module>/src` line sets an upstream
base, then `<sourceSet>/kotlin/ -> src/vendor/<area>/kotlin/` grabs a whole base, then `<sourceSet>/kotlin/ -> src/vendor/<area>/kotlin/` grabs a whole
source set (every `.kt` under it). `!<sourceSet>/kotlin/<pkg>/<File>.kt` refuses source set (every `.kt` under it). `!<sourceSet>/kotlin/<pkg>/<File>.kt` refuses
one file inside a grabbed folder — use it for files hand-vendored + edited under one file inside a grabbed folder - use it for files hand-vendored + edited under
`src/{commonMain,…}` so the folder copy doesn't shadow them, or for upstream `src/{commonMain,…}` so the folder copy doesn't shadow them, or for upstream
files the port doesn't want. A plain `<src> -> <dest>` still pins (or renames on files the port doesn't want. A plain `<src> -> <dest>` still pins (or renames on
copy) a single file. Re-declare `SET_FOLDER` to draw from a second upstream module copy) a single file. Re-declare `SET_FOLDER` to draw from a second upstream module
@@ -271,17 +378,17 @@ manifest in place: commented `# | src -> dest` lines under each folder
directive list what it expands to, and a trailing `# >>> DIAGNOSTIC GAPS` block directive list what it expands to, and a trailing `# >>> DIAGNOSTIC GAPS` block
lists every upstream `.kt` under SET_FOLDER that no directive selects (grouped by lists every upstream `.kt` under SET_FOLDER that no directive selects (grouped by
source set) so new upstream files surface as comments to uncomment. That whole source set) so new upstream files surface as comments to uncomment. That whole
annotated tail is generated — never hand-edit it; edit only the directives up annotated tail is generated - never hand-edit it; edit only the directives up
top and re-run sync. top and re-run sync.
Two categories of code live in each module: Two categories of code live in each module:
1. **Vendored (in `src/vendor/…`)** — copied byte-for-byte from upstream. 1. **Vendored (in `src/vendor/…`)** - copied byte-for-byte from upstream.
Never hand-edit these. If upstream diverges, adjust the manifest or the Never hand-edit these. If upstream diverges, adjust the manifest or the
pinned ref and re-sync. This is the bulk of the codebase (~1500 files pinned ref and re-sync. This is the bulk of the codebase (~1500 files
across the modules). across the modules).
2. **Project code (in `src/commonMain/`, `src/nativeMain/`, 2. **Project code (in `src/commonMain/`, `src/nativeMain/`,
`src/skikoRendererMain/`)** — code we author (project actuals, glue between `src/skikoRendererMain/`)** - code we author (project actuals, glue between
Compose and SDL3, project-specific extensions). Compose and SDL3, project-specific extensions).
### The 5 rules for adding upstream Compose surface ### The 5 rules for adding upstream Compose surface
@@ -299,7 +406,7 @@ Two categories of code live in each module:
`src/vendor/native/kotlin/`). `src/vendor/native/kotlin/`).
3. **If an upstream file needs a small edit to compile / behave 3. **If an upstream file needs a small edit to compile / behave
correctly for us, copy it locally and edit — MANUAL VENDORING, correctly for us, copy it locally and edit - MANUAL VENDORING,
NON-IDEMPOTENT.** Move it OUT of `src/vendor/` into the corresponding NON-IDEMPOTENT.** Move it OUT of `src/vendor/` into the corresponding
`src/{commonMain,nativeMain,…}` tree, add a header comment noting `src/{commonMain,nativeMain,…}` tree, add a header comment noting
which upstream file it derived from and what changed, and comment which upstream file it derived from and what changed, and comment
@@ -312,18 +419,27 @@ Two categories of code live in each module:
// VENDOR-BASE(COMPOSE_REF): <path> @ <tag> ← for umbrella-repo files // VENDOR-BASE(COMPOSE_REF): <path> @ <tag> ← for umbrella-repo files
``` ```
`scripts/compose-fork/check-vendor-drift.py` reads these at every ref A file that is a fresh REIMPLEMENTATION rather than a derived copy (same
bump: it flags any file whose recorded base lags the current pin and package + signatures, different body - see the fallback rule at the end of
this section) gets `// VENDOR-REIMPL: <path> @ <tag>` instead. That marks it
as deliberately NOT drift-tracked: upstream edits to the counterpart need no
reconciling, only a SIGNATURE change does.
`scripts/compose-fork/audit-exclusions.py` classifies every `!` exclusion as
manual vendor / reimpl / orphan and FAILS on any local counterpart carrying
neither marker, so nothing rots invisibly.
`scripts/compose-fork/check-vendor-drift.py` reads the VENDOR-BASE lines at
every ref bump: it flags any file whose recorded base lags the current pin and
(with the local clone) reports whether the upstream base ACTUALLY (with the local clone) reports whether the upstream base ACTUALLY
changed base..pin — i.e. whether the copy needs hand-reconciling or changed base..pin - i.e. whether the copy needs hand-reconciling or
just a ref re-stamp. Now it's a project file — the next sync won't just a ref re-stamp. Now it's a project file - the next sync won't
overwrite it, and future upstream changes to that file need to be overwrite it, and future upstream changes to that file need to be
reconciled by hand. This is fine; do it when the edit is small and reconciled by hand. This is fine; do it when the edit is small and
the file is unlikely to churn upstream. the file is unlikely to churn upstream.
4. **Skiko-specific things go in a `skikoRendererMain` source set.** When 4. **Skiko-specific things go in a `skikoRendererMain` source set.** When
upstream ships a `.skiko.kt` file that uses Skiko's Canvas / Paragraph / …, upstream ships a `.skiko.kt` file that uses Skiko's Canvas / Paragraph / …,
the `.skiko.kt` variant is fine to vendor into `skikoRendererMain` — graphics the `.skiko.kt` variant is fine to vendor into `skikoRendererMain` - graphics
into `:ui-graphics` (`SkiaBackedCanvas.skiko.kt`, `SkiaImageCache.kt`, …), into `:ui-graphics` (`SkiaBackedCanvas.skiko.kt`, `SkiaImageCache.kt`, …),
text into `:ui-text` (`SkiaParagraphEngine.kt`), the render backend into text into `:ui-text` (`SkiaParagraphEngine.kt`), the render backend into
`:ui` (`SkiaRenderBackend.kt`). All native targets attach this source set; `:ui` (`SkiaRenderBackend.kt`). All native targets attach this source set;
@@ -333,14 +449,14 @@ Two categories of code live in each module:
ifdefs.** SDL3 already handles the platform differences for filesystem ifdefs.** SDL3 already handles the platform differences for filesystem
paths (`SDL_GetBasePath`, `SDL_GetPrefPath`), clipboard, cursor, paths (`SDL_GetBasePath`, `SDL_GetPrefPath`), clipboard, cursor,
fullscreen, high-DPI, subprocess launch, MessageBox, and more. fullscreen, high-DPI, subprocess launch, MessageBox, and more.
`file dialog / "reveal in Finder or Explorer" / app-data path` — do `file dialog / "reveal in Finder or Explorer" / app-data path` - do
it through SDL3 first, hand-roll the target-specific version only if it through SDL3 first, hand-roll the target-specific version only if
SDL3 doesn't expose it (currently the file-open/save dialog uses SDL3 doesn't expose it (currently the file-open/save dialog uses
SDL3's `SDL_ShowOpenFileDialog` / `SDL_ShowSaveFileDialog`). SDL3's `SDL_ShowOpenFileDialog` / `SDL_ShowSaveFileDialog`).
If a piece of upstream is too Compose-specific to make sense on our If a piece of upstream is too Compose-specific to make sense on our
stack (e.g. Android AWT layer, iOS `UIView`, JVM `Toolkit`), do a fresh stack (e.g. Android AWT layer, iOS `UIView`, JVM `Toolkit`), do a fresh
reimpl in project code — same signature (same package, same params) so reimpl in project code - same signature (same package, same params) so
call sites don't care. call sites don't care.
## Source-set hierarchy (:ui only) ## Source-set hierarchy (:ui only)
@@ -353,11 +469,11 @@ the `sdl3` cinterop lives in `:sdl-core` (see below). `:ui`'s tree:
commonMain commonMain
└── nativeMain (vendored .native.kt + project native code) └── nativeMain (vendored .native.kt + project native code)
├── skikoRendererMain (Skia drawing pipeline; official Skiko on classpath) ├── skikoRendererMain (Skia drawing pipeline; official Skiko on classpath)
│ ├── skikoRendererMacosMain (macOS-only Skia actuals — Metal bridge) │ ├── skikoRendererMacosMain (macOS-only Skia actuals - Metal bridge)
│ └── skikoRendererLinuxMain (Linux-only Skia actuals — OpenGL) │ └── skikoRendererLinuxMain (Linux-only Skia actuals - OpenGL)
│ attached to: macosArm64Main / linuxX64Main / linuxArm64Main. │ attached to: macosArm64Main / linuxX64Main / linuxArm64Main.
└── skikoRendererMingwSharedMain └── skikoRendererMingwSharedMain
└── skikoRendererMingwMain (mingwX64-only Skia actuals — the bitsycore └── skikoRendererMingwMain (mingwX64-only Skia actuals - the bitsycore
skiko FORK: skiko-windows-x64.dll bound via skiko FORK: skiko-windows-x64.dll bound via
an embedded GNU import lib; OpenGL context) an embedded GNU import lib; OpenGL context)
attached to: mingwX64Main. attached to: mingwX64Main.
@@ -375,22 +491,22 @@ The `sdl3` cinterop lives in the NAKED `:sdl-core` module
(`sdl/sdl-core/src/nativeInterop/cinterop/sdl3.def`: SDL_Window / SDL_Event / (`sdl/sdl-core/src/nativeInterop/cinterop/sdl3.def`: SDL_Window / SDL_Event /
SDL_GetBasePath / clipboard / dialogs / GL+Metal context / SDL_Renderer-for-CPU- SDL_GetBasePath / clipboard / dialogs / GL+Metal context / SDL_Renderer-for-CPU-
raster). `:ui` depends on `:sdl-core` (`api`), so `:ui`'s renderer + SDL↔Compose raster). `:ui` depends on `:sdl-core` (`api`), so `:ui`'s renderer + SDL↔Compose
bridges — and `:desktop-native-window` downstream — see `sdl3.*` and inherit SDL3's static-lib + bridges - and `:desktop-native-window` downstream - see `sdl3.*` and inherit SDL3's static-lib +
linker-opt propagation (the `.def` bakes in `staticLibraries = libSDL3.a` + the linker-opt propagation (the `.def` bakes in `staticLibraries = libSDL3.a` + the
per-OS `linkerOpts`, carried through the klib chain to the final exe, so apps per-OS `linkerOpts`, carried through the klib chain to the final exe, so apps
link SDL just by depending on `:ui`/`:desktop-native-window`). `:sdl-core` has NO Compose link SDL just by depending on `:ui`/`:desktop-native-window`). `:sdl-core` has NO Compose
dependency, so `:ui → :sdl-core` is cycle-free — the compose way (like `ui → skiko`). dependency, so `:ui → :sdl-core` is cycle-free - the compose way (like `ui → skiko`).
## Density flow (Option B — layout in physical pixels) ## Density flow (Option B - layout in physical pixels)
We use the **physical-pixel layout flow** on HiDPI. Concretely: We use the **physical-pixel layout flow** on HiDPI. Concretely:
- `SDL_WINDOW_HIGH_PIXEL_DENSITY` is set on the SDL window. - `SDL_WINDOW_HIGH_PIXEL_DENSITY` is set on the SDL window.
- `LocalDensity` = the DPR (2.0 on Retina, 1.0 otherwise) — from - `LocalDensity` = the DPR (2.0 on Retina, 1.0 otherwise) - from
`pixelWidth / windowWidth`. `pixelWidth / windowWidth`.
- Constraints passed to `rootNode.measure(…)` are the **physical pixel** size, - Constraints passed to `rootNode.measure(…)` are the **physical pixel** size,
not logical points. not logical points.
- `renderBackend.beginFrame(1f)` — no renderer-side scale; layout already ran - `renderBackend.beginFrame(1f)` - no renderer-side scale; layout already ran
in physical pixels. in physical pixels.
- Pointer coords from SDL are logical points; they're multiplied by DPR - Pointer coords from SDL are logical points; they're multiplied by DPR
before dispatch so the whole event pipeline is in the same physical-px before dispatch so the whole event pipeline is in the same physical-px
@@ -399,7 +515,7 @@ We use the **physical-pixel layout flow** on HiDPI. Concretely:
Consequence: `Modifier.width(20.dp)` at density 2 → 40 physical pixels wide. Consequence: `Modifier.width(20.dp)` at density 2 → 40 physical pixels wide.
`onSizeChanged { it.width }` reports physical pixels. If you're passing pixel `onSizeChanged { it.width }` reports physical pixels. If you're passing pixel
integers into pixel-based modifiers, use the lambda forms integers into pixel-based modifiers, use the lambda forms
(`Modifier.offset { IntOffset(x, y) }`) — they take pixels directly. (`Modifier.offset { IntOffset(x, y) }`) - they take pixels directly.
Passing raw `px.dp` will double-scale on Retina. Passing raw `px.dp` will double-scale on Retina.
## Building ## Building
@@ -413,14 +529,14 @@ Passing raw `px.dp` will double-scale on Retina.
./gradlew :demo:runDebugExecutableLinuxX64 ./gradlew :demo:runDebugExecutableLinuxX64
./gradlew :apidemo:runDebugExecutableLinuxX64 ./gradlew :apidemo:runDebugExecutableLinuxX64
# Windows (from Windows — mingw cross-build from macOS/Linux fails at cinterop). # Windows (from Windows - mingw cross-build from macOS/Linux fails at cinterop).
# Links the bitsycore skiko FORK from GitHub Packages (com.bitsycore.skiko:skiko: # Links the bitsycore skiko FORK from maven.bitsycore.com (com.bitsycore.skiko:skiko:
# 0.150.1-mingw.1, override -PskikoMingwVersion); the bridge plugin drops # 0.150.1-mingw.2, override -PskikoMingwVersion); the bridge plugin drops
# skiko-windows-x64.dll next to the exe (installWindowsSkiaDll). # skiko-windows-x64.dll next to the exe (installWindowsSkiaDll).
gradlew.bat :demo:runDebugExecutableMingwX64 gradlew.bat :demo:runDebugExecutableMingwX64
gradlew.bat :apidemo:runDebugExecutableMingwX64 gradlew.bat :apidemo:runDebugExecutableMingwX64
# Stock JVM Compose Desktop (any host) — the parity reference: the SAME shared # Stock JVM Compose Desktop (any host) - the parity reference: the SAME shared
# screens on upstream Compose; differences vs the native build = port bugs. # screens on upstream Compose; differences vs the native build = port bugs.
./gradlew :demo:run ./gradlew :demo:run
./gradlew :apidemo:run ./gradlew :apidemo:run
@@ -429,41 +545,41 @@ gradlew.bat :apidemo:runDebugExecutableMingwX64
### System dependencies ### System dependencies
SDL3 is **built from source as a static library on every OS** and linked SDL3 is **built from source as a static library on every OS** and linked
straight into the executable — no brew/apt SDL packages. Build it once per host straight into the executable - no brew/apt SDL packages. Build it once per host
with `python scripts/build-sdl/build-all.py`. Per-host toolchain requirements with `python scripts/build-sdl/build-all.py`. Per-host toolchain requirements
and the step breakdown are in [TOOLING.md](TOOLING.md#native-libraries). and the step breakdown are in [TOOLING.md](TOOLING.md#native-libraries).
Skia comes in through the Skiko klibs: the official Maven ones on macOS/Linux Skia comes in through the Skiko klibs: the official Maven ones on macOS/Linux
(no runtime .so/.dylib — Skia is statically inside the klib), and the bitsycore (no runtime .so/.dylib - Skia is statically inside the klib), and the bitsycore
fork on Windows, whose `skiko-windows-x64.dll` the bridge plugin provisions next fork on Windows, whose `skiko-windows-x64.dll` the bridge plugin provisions next
to the exe. So a macOS/Linux distributable is `<app>` + `data.kres`; a Windows to the exe. So a macOS/Linux distributable is `<app>` + `data.kres`; a Windows
one is `<app>.exe` + `data.kres` + `skiko-windows-x64.dll`. one is `<app>.exe` + `data.kres` + `skiko-windows-x64.dll`.
## Runtime bundling — data.kres ## Runtime bundling - data.kres
Every app ships `<app>.exe` + `data.kres` (a STORED zip alongside the Every app ships `<app>.exe` + `data.kres` (a STORED zip alongside the
executable, loaded via `SDL_GetBasePath()`). Contents: executable, loaded via `SDL_GetBasePath()`). Contents:
- App drawables + files under `composeResources/{drawable,files}/` - App drawables + files under `composeResources/{drawable,files}/`
- `font/NotoSans.ttf` — the default variable font. Bundling it is the APP's - `font/NotoSans.ttf` - the default variable font. Bundling it is the APP's
job: each app opts in via `registerComposeFontBundling { bundleNotoSans = job: each app opts in via `registerComposeFontBundling { bundleNotoSans =
true; … }` (buildSrc — every flag is opt-in, no flag = no-op), which true; … }` (buildSrc - every flag is opt-in, no flag = no-op), which
registers its `downloadNotoFonts` task (into `<app>/build/fonts/`); the registers its `downloadNotoFonts` task (into `<app>/build/fonts/`); the
library ships no download task. Pass `-PbundleDefaultFont=false` to skip. library ships no download task. Pass `-PbundleDefaultFont=false` to skip.
- `font/NotoSansMono.ttf` — `autoDetectNotoSansMono = true` bundles it when - `font/NotoSansMono.ttf` - `autoDetectNotoSansMono = true` bundles it when
`FontFamily.Monospace` appears in the app sources (demo); `FontFamily.Monospace` appears in the app sources (demo);
`bundleNotoSansMono = true` forces it (apidemo's body font, loaded through `bundleNotoSansMono = true` forces it (apidemo's body font, loaded through
its own seam) its own seam)
- Material Symbols fonts for the styles the app **actually uses** - Material Symbols fonts for the styles the app **actually uses**
(`bundleMaterialSymbols = true`) — the Zip task scans the app's Kotlin (`bundleMaterialSymbols = true`) - the Zip task scans the app's Kotlin
sources for `MaterialSymbolsOutlined` / `Rounded` / `Sharp` and only sources for `MaterialSymbolsOutlined` / `Rounded` / `Sharp` and only
bundles the fonts referenced. bundles the fonts referenced.
- `-PsubsetIcons=true` (default on, opted into per app via - `-PsubsetIcons=true` (default on, opted into per app via
`enableIconSubsetting = true` — apidemo yes, demo no): `enableIconSubsetting = true` - apidemo yes, demo no):
`scripts/subset-material-symbols.py` scans app sources for `scripts/subset-material-symbols.py` scans app sources for
`MaterialSymbols.<Name>` usage and hb-subsets each bundled font down to `MaterialSymbols.<Name>` usage and hb-subsets each bundled font down to
just those glyphs. Needs `hb-subset` on PATH (`brew install harfbuzz` / just those glyphs. Needs `hb-subset` on PATH (`brew install harfbuzz` /
`apt install harfbuzz-utils`) — falls back to the full font if absent. `apt install harfbuzz-utils`) - falls back to the full font if absent.
## Vendor sync workflow ## Vendor sync workflow
@@ -472,61 +588,75 @@ executable, loaded via `SDL_GetBasePath()`). Contents:
scripts/compose-fork/sync.sh scripts/compose-fork/sync.sh
``` ```
Upstream ref: `scripts/compose-fork/compose.properties` — set to a durable tag Upstream refs live in `scripts/compose-fork/compose.properties`, one `NAME=value`
of `JetBrains/compose-multiplatform-core`. Bump the ref → re-sync → let the per repo, each pinned to a durable TAG. Five repos are vendored now:
`COMPOSE_CORE_REF` (JetBrains/compose-multiplatform-core), `COMPOSE_REF` (the
compose-multiplatform umbrella), `KOIN_REF` (InsertKoinIO/koin), `COIL_REF`
(coil-kt/coil) and `PULSE_REF` (bitsycore/pulse-mvi). Each gets its own sibling
sparse clone (`../cmp-ref`, `../cmp-ref-koin`, …). Bump the ref → re-sync → let the
build tell you what broke. Per-module sync, manifest re-formatting, and the build tell you what broke. Per-module sync, manifest re-formatting, and the
drift / vendor-clean guardrails are in drift / vendor-clean guardrails are in
[TOOLING.md](TOOLING.md#vendoring-upstream-compose). [TOOLING.md](TOOLING.md#vendoring-upstream-compose).
## Key files by area — start here when you need to find something ## Key files by area - start here when you need to find something
### Renderer + main loop ### Renderer + main loop
- `compose/desktop/native/window/src/nativeMain/…/ComposeWindow.kt` — main loop, - `compose/desktop/native/desktop-native-window/src/nativeMain/…/` - the window
recomposer lifecycle, SDL event dispatch, composition-local seeding. module, one concern per file:
- `compose/ui/ui/src/nativeMain/…/RenderBackend.kt` — the interface. - `ComposeWindow.kt` - the PUBLIC surface (`ApplicationScope`, `Window()`,
- `compose/ui/ui/src/nativeMain/…/GpuMode.kt` — sealed driver picker `nativeComposeApp`, `nativeComposeWindow`) + the main loop.
- `WindowInstance.kt` - one SDL window: backend + render backend + root host +
its own recomposer/composition, event handling, frame pump, teardown. This is
where composition-local seeding lives.
- `AppRuntime.kt` - the live-window registry + create/destroy lifecycle.
- `WindowAttributes.kt` - the Compose Desktop `Window()` attribute bundle.
- `FrameTiming.kt` - deterministic/virtual frame clocks and the
probe-quiescence hooks (`disableInfiniteAnimations`, `useVirtualFrameTime`,
`windowHasInvalidations`).
- `compose/ui/ui/src/nativeMain/…/RenderBackend.kt` - the interface.
- `compose/ui/ui/src/nativeMain/…/GpuMode.kt` - sealed driver picker
(`Auto` / `Software` / `Skia.OpenGL` / `Skia.Metal`). (`Auto` / `Software` / `Skia.OpenGL` / `Skia.Metal`).
- `compose/ui/ui/src/skikoRendererMain/…/renderer/skia/SkiaRenderBackend.kt` — - `compose/ui/ui/src/skikoRendererMain/…/renderer/skia/SkiaRenderBackend.kt` -
the Skia render backend (shared by every native target). the Skia render backend (shared by every native target).
- `compose/ui/ui/src/skikoRendererMingwMain/…` — mingwX64-only Skia actuals - `compose/ui/ui/src/skikoRendererMingwMain/…` - mingwX64-only Skia actuals
bound against the skiko fork's flat extern-C surface. bound against the skiko fork's flat extern-C surface.
### Layout / composition wiring ### Layout / composition wiring
- `compose/ui/ui/src/commonMain/…/node/ComposeRootHost.kt` — root LayoutNode - `compose/ui/ui/src/commonMain/…/node/ComposeRootHost.kt` - root LayoutNode
host, hit-test, event dispatch, snapshot observer. host, hit-test, event dispatch, snapshot observer.
- `compose/ui/ui/src/commonMain/…/node/impl/ComposeOwner.kt` — the - `compose/ui/ui/src/commonMain/…/node/impl/ComposeOwner.kt` - the
project `Owner` implementation + `ProjectOwnedLayer` (graphicsLayer / clip / project `Owner` implementation + `ProjectOwnedLayer` (graphicsLayer / clip /
alpha bridge). alpha bridge).
- `compose/ui/ui/src/commonMain/…/node/NodeApplier.kt`. - `compose/ui/ui/src/commonMain/…/node/NodeApplier.kt`.
### Text (all in `:ui-text` now) ### Text (all in `:ui-text` now)
- `compose/ui/ui-text/src/nativeMain/…/ui/text/SkiaParagraph.native.kt` — the - `compose/ui/ui-text/src/nativeMain/…/ui/text/SkiaParagraph.native.kt` - the
`Paragraph` actual (skiko-free; the 30 interface methods over plain data), `Paragraph` actual (skiko-free; the 30 interface methods over plain data),
driving the skiko engine through the `NativeParagraphOps` seam. driving the skiko engine through the `NativeParagraphOps` seam.
- `compose/ui/ui-text/src/skikoRendererMain/…/renderer/skia/SkiaParagraphEngine.kt` - `compose/ui/ui-text/src/skikoRendererMain/…/renderer/skia/SkiaParagraphEngine.kt`
— `SkiaParagraphOps` (builds/queries the real skiko `skparagraph`) + `SkiaFonts` - `SkiaParagraphOps` (builds/queries the real skiko `skparagraph`) + `SkiaFonts`
(data.kres bytes → skiko Typeface; the port's family/variable-axis model). (data.kres bytes → skiko Typeface; the port's family/variable-axis model).
- `compose/ui/ui-text/src/nativeMain/…/ui/text/ParagraphFactories.native.kt` — - `compose/ui/ui-text/src/nativeMain/…/ui/text/ParagraphFactories.native.kt` -
actuals for the `Paragraph(…)` / `ParagraphIntrinsics(…)` factory family. actuals for the `Paragraph(…)` / `ParagraphIntrinsics(…)` factory family.
### Icons ### Icons
- `compose/foundation/foundation/src/nativeMain/…/icons/IconFontIcon.kt` — - `compose/foundation/foundation/src/nativeMain/…/icons/IconFontIcon.kt` -
codepoint-based `Icon` composable + `MaterialIconAxes` / codepoint-based `Icon` composable + `MaterialIconAxes` /
`MaterialIconAxisDefaults`. `MaterialIconAxisDefaults`.
- `utils/material-symbols/src/…/MaterialSymbols{Outlined,Rounded,Sharp}.kt`. - `utils/material-symbols/src/…/MaterialSymbols{Outlined,Rounded,Sharp}.kt`.
### Resources ### Resources
- `compose/ui/ui/src/commonMain/…/res/Res.kt` — the project's - `compose/ui/ui/src/commonMain/…/res/Res.kt` - the project's
`androidx.compose.ui.res` reimpl (`Painter`, `ImageLoader`). `androidx.compose.ui.res` reimpl (`Painter`, `ImageLoader`).
- `compose/ui/ui/src/nativeMain/…/ResourceIO.kt` — opens `data.kres` once via - `compose/ui/ui/src/nativeMain/…/ResourceIO.kt` - opens `data.kres` once via
`SDL_GetBasePath()` + parses central directory; each entry served by an `SDL_GetBasePath()` + parses central directory; each entry served by an
`fseek + fread`. `fseek + fread`.
### Apps ### Apps
- `demo/src/nativeMain/kotlin/Main.kt` — sidebar demo (`--gpu`, `--screen`, - `demo/src/nativeMain/kotlin/Main.kt` - sidebar demo (`--gpu`, `--screen`,
`--screenshot`). `--screenshot`).
- `apidemo/src/nativeMain/kotlin/Main.kt` — API manager entry. - `apidemo/src/nativeMain/kotlin/Main.kt` - API manager entry.
`apidemo/src/nativeMain/kotlin/UiCompat.kt` — project-local `apidemo/src/nativeMain/kotlin/UiCompat.kt` - project-local
`Dialog` / `DropdownMenu` / `DropdownMenuItem` / `TooltipBox` (m3 doesn't `Dialog` / `DropdownMenu` / `DropdownMenuItem` / `TooltipBox` (m3 doesn't
ship drop-in equivalents for our anchor / scrim patterns). ship drop-in equivalents for our anchor / scrim patterns).
@@ -547,9 +677,23 @@ Open renderer / fidelity / release work is tracked in [PLAN.md](PLAN.md).
## Conventions ## Conventions
Kotlin standard style — plain `camelCase` for parameters, local variables, Kotlin standard style for formatting (braces, wrapping, indentation).
and fields. **No `f`/`in`/`v` prefixes**, no SPIRTECH scheme (see the global
CLAUDE.md — that scheme is C-only). Naming, as the code actually is - this paragraph used to claim "no prefixes",
which had not been true for a long time:
- **`m` for private/protected members** - `mWidth`, `mSurface`, `mCache`. NOT
`f`: the `f` prefix was retired across the project, so do not reintroduce it.
- **`v` for local variables** - `vWindow`, `vResult`.
- **`in` for function parameters** - `inTitle`, `inWidth`. Note Compose
`@Composable` parameters are the exception: they stay plain, because the name
is part of the call-site API (`title = `, `onCloseRequest = `).
- **`k` for private file-level constants** - `kMaxFrames`, `kIsWindows`.
- Public API surface is plain `camelCase` - prefixes are for implementation
detail only, and nothing prefixed should ever reach the klib dump.
VENDORED code under `src/vendor/` follows UPSTREAM's naming, untouched - it is
copied byte-for-byte, so never re-prefix anything in it.
Section headers inside a file, when useful: Section headers inside a file, when useful:
@@ -566,31 +710,31 @@ In-function smaller scope:
// Name // Name
``` ```
Function-level comments only where the name isn't self-documenting — avoid Function-level comments only where the name isn't self-documenting - avoid
line-by-line commentary. Prefer KDoc (`/** … */`) over `/* … */` for docs line-by-line commentary. Prefer KDoc (`/** … */`) over `/* … */` for docs
that should surface in tooling. that should surface in tooling.
## Common pitfalls ## Common pitfalls
- **State changes don't repaint the UI** — check - **State changes don't repaint the UI** - check
`Snapshot.sendApplyNotifications()` is being called each frame in the main `Snapshot.sendApplyNotifications()` is being called each frame in the main
loop (`ComposeWindow.kt`). Without it, `mutableStateOf` writes never loop (`ComposeWindow.kt`). Without it, `mutableStateOf` writes never
reach the recomposer. reach the recomposer.
- **Physical-pixel modifiers double-scale on Retina** — under Option-B - **Physical-pixel modifiers double-scale on Retina** - under Option-B
density, layout runs in physical pixels but `Modifier.width(20.dp)` still density, layout runs in physical pixels but `Modifier.width(20.dp)` still
goes through `density.toPx()`. If you have a value already in physical goes through `density.toPx()`. If you have a value already in physical
pixels (from `onSizeChanged`), convert it back to Dp via pixels (from `onSizeChanged`), convert it back to Dp via
`with(LocalDensity.current) { pxInt.toDp() }` before passing to `with(LocalDensity.current) { pxInt.toDp() }` before passing to
`Modifier.width(…)`, or use pixel-based lambdas `Modifier.width(…)`, or use pixel-based lambdas
(`Modifier.offset { IntOffset(x, y) }`). (`Modifier.offset { IntOffset(x, y) }`).
- **Skia's `saveLayer(bounds, paint)`** — GPU backends allocate the offscreen - **Skia's `saveLayer(bounds, paint)`** - GPU backends allocate the offscreen
to `bounds`. If content inside translates beyond those bounds, it gets to `bounds`. If content inside translates beyond those bounds, it gets
clipped. `SkiaCanvas.saveLayer` passes a huge fixed bounds to sidestep this. clipped. `SkiaCanvas.saveLayer` passes a huge fixed bounds to sidestep this.
- **`Modifier.alpha` clips to bounds** — upstream contract: - **`Modifier.alpha` clips to bounds** - upstream contract:
`Modifier.alpha(x)` desugars to `graphicsLayer(alpha = x, clip = true)`. `Modifier.alpha(x)` desugars to `graphicsLayer(alpha = x, clip = true)`.
For a drag ghost that also translates, put `alpha` and `translationX` on For a drag ghost that also translates, put `alpha` and `translationX` on
the SAME `graphicsLayer(...)` so clip stays false. the SAME `graphicsLayer(...)` so clip stays false.
- **Substituted Maven modules hide their transitives from common metadata** — - **Substituted Maven modules hide their transitives from common metadata** -
when the bridge swaps an official coord for a project module on native when the bridge swaps an official coord for a project module on native
configs, KGP's granular-metadata visibility check drops that Maven module's configs, KGP's granular-metadata visibility check drops that Maven module's
TRANSITIVES from the commonMain classpath (symptom: `Unresolved reference TRANSITIVES from the commonMain classpath (symptom: `Unresolved reference
@@ -598,74 +742,95 @@ that should surface in tooling.
while per-target compilation is fine). Declare EVERY artifact the common while per-target compilation is fine). Declare EVERY artifact the common
code touches DIRECTLY (ui-graphics, ui-text, ui-unit, …) and give each its code touches DIRECTLY (ui-graphics, ui-text, ui-unit, …) and give each its
own bridge rule. Note only the WINDOWS publish job compiles common metadata own bridge rule. Note only the WINDOWS publish job compiles common metadata
(it owns the root KotlinMultiplatform publications — the only host that (it owns the root KotlinMultiplatform publications - the only host that
declares every target, so only its .module files carry the full variant declares every target, so only its .module files carry the full variant
table; macOS-published roots left v0.1.15 without mingwX64 variants) — table; macOS-published roots left v0.1.15 without mingwX64 variants) -
test with `gradlew :<module>:compileCommonMainKotlinMetadata` before tagging. test with `gradlew :<module>:compileCommonMainKotlinMetadata` before tagging.
## Useful Gradle tricks ## Useful Gradle tricks
- `--args="--gpu=skia.opengl --screen=Buttons"` — pass CLI to the demo. - `--args="--gpu=skia.opengl --screen=Buttons"` - pass CLI to the demo.
- `--info` — see cinterop classpath + include paths actually used. - `--info` - see cinterop classpath + include paths actually used.
- After a module rename or IC-cache mismatch: nuke - After a module rename or IC-cache mismatch: nuke
`demo/build/kotlin-native-ic-cache` (or `apidemo/build/…`). Kotlin/Native `demo/build/kotlin-native-ic-cache` (or `apidemo/build/…`). Kotlin/Native
pins module IDs into its klib metadata; a stale cache surfaces as pins module IDs into its klib metadata; a stale cache surfaces as
`Unknown dependent library com.bitsycore.compose.sdl:core` (or `Unknown dependent library com.bitsycore.compose.sdl:core` (or
whatever the old module name was). whatever the old module name was).
## Known Compatible — official Maven KMP artifacts used AS-IS ## Known Compatible - official Maven KMP artifacts used AS-IS
Before reimplementing or vendoring ANYTHING androidx, check Maven: a lot of Before reimplementing or vendoring ANYTHING androidx, check Maven: a lot of
the architecture stack publishes real Kotlin/Native desktop klibs the architecture stack publishes real Kotlin/Native desktop klibs
(mingwX64 + linuxX64/arm64 + macosArm64) and runs on this port unmodified. (mingwX64 + linuxX64/arm64 + macosArm64) and runs on this port unmodified.
Sometimes the GOOGLE coordinates (`androidx.*`) have the K/N variant, Sometimes the GOOGLE coordinates (`androidx.*`) have the K/N variant,
sometimes the JETBRAINS ones (`org.jetbrains.*`) — check both before sometimes the JETBRAINS ones (`org.jetbrains.*`) - check both before
concluding an artifact "doesn't exist" for our targets. concluding an artifact "doesn't exist" for our targets.
Verified in-tree (api-exposed by `:ui` unless noted): Verified in-tree (api-exposed by `:ui` unless noted):
- `org.jetbrains.compose.runtime:runtime` / `runtime-saveable` 1.11.1 — - `org.jetbrains.compose.runtime:runtime` / `runtime-saveable` 1.11.1 -
THE Compose runtime (composition, snapshots, recomposer). Never vendored. THE Compose runtime (composition, snapshots, recomposer). Never vendored.
- `androidx.compose.runtime:runtime-retain` 1.11.1 (google coordinates). - `androidx.compose.runtime:runtime-retain` 1.11.1 (google coordinates).
- `androidx.lifecycle:*` **2.11.0** (google): `lifecycle-runtime-compose`, - `androidx.lifecycle:*` **2.11.0** (google): `lifecycle-runtime-compose`,
`lifecycle-viewmodel`, `lifecycle-viewmodel-compose`, `lifecycle-viewmodel`, `lifecycle-viewmodel-compose`,
`lifecycle-viewmodel-savedstate`, **`lifecycle-viewmodel-navigation3`** — `lifecycle-viewmodel-savedstate`, **`lifecycle-viewmodel-navigation3`** -
the whole ViewModel + SavedStateHandle + nav3-decorator stack needed ZERO the whole ViewModel + SavedStateHandle + nav3-decorator stack needed ZERO
reimplementation; only the window-side owners had to be provided (see reimplementation; only the window-side owners had to be provided (see
caveats). caveats).
- `androidx.savedstate:savedstate` / `savedstate-compose` **1.5.0** (google). - `androidx.savedstate:savedstate` / `savedstate-compose` **1.5.0** (google).
- `androidx.navigation3:navigation3-runtime` **1.1.4** — backstack / NavEntry - `androidx.navigation3:navigation3-runtime` **1.1.4** - backstack / NavEntry
/ decorators. Only navigation3-UI (NavDisplay) lacks a K/N desktop artifact / decorators. Only navigation3-UI (NavDisplay) lacks a K/N desktop artifact
→ vendored as `:navigation3-ui`. → vendored as `:navigation3-ui`.
- `androidx.navigationevent:navigationevent-compose` 1.1.2 — predictive-back - `androidx.navigationevent:navigationevent-compose` 1.1.2 - predictive-back
event plumbing (BackHandler, NavDisplay gestures). event plumbing (BackHandler, NavDisplay gestures).
- `androidx.collection:collection` — plain Maven dep, not a module. - `androidx.collection:collection` - plain Maven dep, not a module.
- `androidx.window:window-core` **1.5.0** - WindowSizeClass / the adaptive
window model. Publishes mingwX64 + linux + macos under BOTH coordinate sets;
the port takes the google one like the rest of the androidx stack. This is
what makes the material3-adaptive vendoring a pure re-target: the dependency
everybody assumed was the blocker was never missing.
- `io.insert-koin:koin-core` **4.2.2** - the DI container itself publishes
mingwX64 + linux + macos. Only Koin's viewmodel / compose layers stop at
apple+android and are vendored (`koin/`).
- `androidx.navigationevent:navigationevent-compose` 1.1.2 AND the jetbrains
`org.jetbrains.androidx.navigationevent:navigationevent-compose` 1.1.0 - BOTH
publish mingwX64 + linux + macos. Nothing to vendor here.
- NOT compatible (vendored instead): `components-resources` (no mingw/linux - NOT compatible (vendored instead): `components-resources` (no mingw/linux
klibs → `:components-resources`), `navigation3-ui` (same → `:navigation3-ui`). klibs → `:components:resources:components-resources`), `navigation3-ui` (same →
`:navigation3:navigation3-ui`), Koin's viewmodel/compose modules (→ `koin/`),
the whole Coil 3 stack (no mingwX64 anywhere; no desktop native at all for its
compose layer → `coil/`), and Pulse MVI (no desktop native → `pulse/`).
- **Always check BOTH coordinate sets before concluding an artifact has no
desktop-native klibs.** `androidx.*` and `org.jetbrains.androidx.*` are
different artifacts with the same classes; sometimes only one carries the K/N
variant, and where both do, pick ONE - putting both on the path duplicates
every class. This port standardises on the GOOGLE `androidx.lifecycle:*`
coordinates, so vendored upstream code that referenced the jetbrains mirrors
is repointed at the google ones in the module's build file.
- Infra: `kotlinx-coroutines-core`, `atomicfu`, `okio`, - Infra: `kotlinx-coroutines-core`, `atomicfu`, `okio`,
`kotlinx-serialization`. `kotlinx-serialization`.
Caveats that make these work here (all already wired — listed so nobody Caveats that make these work here (all already wired - listed so nobody
"fixes" them away): "fixes" them away):
- The google `LocalViewModelStoreOwner` / `LocalSavedStateRegistryOwner` / - The google `LocalViewModelStoreOwner` / `LocalSavedStateRegistryOwner` /
`LocalLifecycleOwner` are PLAIN composition locals — the JB HostDefault `LocalLifecycleOwner` are PLAIN composition locals - the JB HostDefault
mechanism (`compositionLocalWithHostDefaultOf`) does not exist in google mechanism (`compositionLocalWithHostDefaultOf`) does not exist in google
artifacts. `WindowArchitectureOwner` (ComposeWindow.kt) provides all three artifacts. `WindowArchitectureOwner` (WindowInstance.kt) provides all three
per window, mirrors upstream desktop's DefaultArchitectureComponentsOwner, per window, mirrors upstream desktop's DefaultArchitectureComponentsOwner,
calls `enableSavedStateHandles()` at construction, and follows SDL focus / calls `enableSavedStateHandles()` at construction, and follows SDL focus /
visibility (focused → RESUMED, unfocused → STARTED, minimised → CREATED). visibility (focused → RESUMED, unfocused → STARTED, minimised → CREATED).
- The window composes its FIRST composition at CREATED and resumes after — - The window composes its FIRST composition at CREATED and resumes after -
`enableSavedStateHandles()` callers running in composition require `enableSavedStateHandles()` callers running in composition require
lifecycle ≤ CREATED. Related contract: `rememberViewModelStoreOwner()` with lifecycle ≤ CREATED. Related contract: `rememberViewModelStoreOwner()` with
its default `savedStateRegistryOwner` THROWS at a RESUMED call site (same its default `savedStateRegistryOwner` THROWS at a RESUMED call site (same
on Android) — scope shared VMs to the window owner (`viewModel { }` outside on Android) - scope shared VMs to the window owner (`viewModel { }` outside
the entries, the `activityViewModels()` analog) instead. the entries, the `activityViewModels()` analog) instead.
- nav3 entry decorators: saveable BEFORE viewmodel — - nav3 entry decorators: saveable BEFORE viewmodel -
`listOf(rememberSaveableStateHolderNavEntryDecorator(), `listOf(rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator())`. rememberViewModelStoreNavEntryDecorator())`.
- `Dispatchers.Main.immediate` must run inline on the main thread - `Dispatchers.Main.immediate` must run inline on the main thread
(Sdl3MainDispatcher) — androidx.lifecycle's main-thread enforcement (Sdl3MainDispatcher) - androidx.lifecycle's main-thread enforcement
round-trips through it; a queue-only Main deadlocks composition. round-trips through it; a queue-only Main deadlocks composition.
- Regression probes: `demo --nav3test` (nav3 + ViewModels + lifecycle, - Regression probes: `demo --nav3test` (nav3 + ViewModels + lifecycle,
composed late at RESUMED like the real sidebar flow), `--backtest` composed late at RESUMED like the real sidebar flow), `--backtest`
@@ -673,4 +838,4 @@ Caveats that make these work here (all already wired — listed so nobody
## License ## License
MIT — see [LICENSE.md](LICENSE.md). MIT - see [LICENSE.md](LICENSE.md).
+384 -154
View File
@@ -1,7 +1,7 @@
# ROAD TO 1.0.0 # ROAD TO 1.0.0
**Objective:** get ComposeNativeSDL3 **as close to upstream Compose Multiplatform as **Objective:** get ComposeNativeSDL3 **as close to upstream Compose Multiplatform as
possible** — vendor every file we can instead of rewriting, stay performant, and look possible** - vendor every file we can instead of rewriting, stay performant, and look
faithful (ideally pixel-identical across macOS / Linux / Windows). The port is already at faithful (ideally pixel-identical across macOS / Linux / Windows). The port is already at
99% upstream API coverage with maximal vendoring and no steady-state perf gap on 99% upstream API coverage with maximal vendoring and no steady-state perf gap on
macOS/Metal; the remaining 1.0.0 work is **cross-platform rendering fidelity** (Windows macOS/Metal; the remaining 1.0.0 work is **cross-platform rendering fidelity** (Windows
@@ -9,87 +9,87 @@ skiko-fork divergences), **completing native-actual stubs**, and the release mec
(1.12.0-stable re-pin + WIN-SMOKE). (1.12.0-stable re-pin + WIN-SMOKE).
**Status / handoff:** the macOS/Linux-verifiable work is done and committed on `main` (see **Status / handoff:** the macOS/Linux-verifiable work is done and committed on `main` (see
**"Landed toward 1.0.0"** below — don't redo it). The remaining blockers need a **Windows host** **"Landed toward 1.0.0"** below - don't redo it). The remaining blockers need a **Windows host**
→ jump to **"▶ Continuing on Windows (mingwX64) — START HERE"**. This file is self-contained: it → jump to **"▶ Continuing on Windows (mingwX64) - START HERE"**. This file is self-contained: it
has the build/run commands, the exact confirm steps, the macOS baseline data to diff against, and has the build/run commands, the exact confirm steps, the macOS baseline data to diff against, and
per-task fixes. So on Windows you can say **"continue PLAN.md"** and have everything. per-task fixes. So on Windows you can say **"continue PLAN.md"** and have everything.
## Definition of done for 1.0.0 ## Definition of done for 1.0.0
- [x] No text tofu on any platform — Windows tab fix **CONFIRMED on-device** (§1a, 2026-08-04: - [x] No text tofu on any platform - Windows tab fix **CONFIRMED on-device** (§1a, 2026-08-04:
literal `\t`→space, no `.notdef`); CJK / color-emoji / control chars render clean too (§1c). literal `\t`→space, no `.notdef`); CJK / color-emoji / control chars render clean too (§1c).
- [x] Fork-vs-official divergence surface (FontMgr, gamma, ICU) **audited on native Windows** - [x] Fork-vs-official divergence surface (FontMgr, gamma, ICU) **audited on native Windows**
(2026-08-04): metrics == JVM (§1b), FontMgr fallback works (CJK + color emoji, §1c), ICU (2026-08-04): metrics == JVM (§1b), FontMgr fallback works (CJK + color emoji, §1c), ICU
classification works (§1c). Net — the fork's Skia matches upstream; the only deltas are its classification works (§1c). Net - the fork's Skia matches upstream; the only deltas are its
thin extern-C bindings, handled in shared Kotlin (LineMetrics reconstruction). No fork-side thin extern-C bindings, handled in shared Kotlin (LineMetrics reconstruction). No fork-side
fix needed for 1.0. fix needed for 1.0.
- [~] Native-actual fidelity blockers closed: float pointer coords **DONE**, screen-reader - [~] Native-actual fidelity blockers closed: float pointer coords **DONE**, screen-reader
no-op **DONE**, text context menu **RECONCILED — already works** via the legacy path no-op **DONE**, text context menu **RECONCILED - already works** via the legacy path
(§2). Date/time localization is P1 polish (formatter already works). (§2). Date/time localization is P1 polish (formatter already works).
- [x] Vendor hygiene stays clean: zero drift (all 10 manual vendors match pin), zero - [x] Vendor hygiene stays clean: zero drift (all 10 manual vendors match pin), zero
commonMain rule-1 violations, zero vendored files touched this session — verified via commonMain rule-1 violations, zero vendored files touched this session - verified via
`check-vendor-drift.py`. `check-vendor-drift.py`.
- [~] SDL static lib slimmed to the used subsystem surface (§3) — **DONE + verified on - [~] SDL static lib slimmed to the used subsystem surface (§3) - **DONE + verified on
macOS/Metal**; other hosts pending WIN-SMOKE. macOS/Metal**; other hosts pending WIN-SMOKE.
- [ ] Refs re-pinned to Compose **1.12.0 stable**, JVM parity versions bumped, WIN-SMOKE - [ ] Refs re-pinned to Compose **1.12.0 stable**, JVM parity versions bumped, WIN-SMOKE
fidelity pass green on a real Windows host. fidelity pass green on a real Windows host.
- [x] `CLAUDE.md` documentation map consistent with tree — line-1 typo fixed; the stale - [x] `CLAUDE.md` documentation map consistent with tree - line-1 typo fixed; the stale
historical docs `RENDERER.md` + `SKIKO-MINGW-FEASIBILITY.md` removed (current renderer historical docs `RENDERER.md` + `SKIKO-MINGW-FEASIBILITY.md` removed (current renderer
essentials inlined into CLAUDE.md, the rest is in git history); all dead doc links essentials inlined into CLAUDE.md, the rest is in git history); all dead doc links
(TODO.md / RENDERER.md / SKIKO) removed or repointed to PLAN.md. (TODO.md / RENDERER.md / SKIKO) removed or repointed to PLAN.md.
## Landed toward 1.0.0 (this pass — all build + run verified on macOS/Metal) ## Landed toward 1.0.0 (this pass - all build + run verified on macOS/Metal)
Everything below is committed; details + file refs are in the sections that follow. Everything below is committed; details + file refs are in the sections that follow.
1. **Tab tofu fix** (§1a P0) — normalize `\t`→space before shaping so the Windows fork 1. **Tab tofu fix** (§1a P0) - normalize `\t`→space before shaping so the Windows fork
can't render `.notdef`. Platform-independent, length-preserving. can't render `.notdef`. Platform-independent, length-preserving.
2. **HiDPI caret quantization fix** (§2 P0) — pointer/wheel coords carried as `Float` 2. **HiDPI caret quantization fix** (§2 P0) - pointer/wheel coords carried as `Float`
end-to-end instead of truncated to `Int`. end-to-end instead of truncated to `Int`.
3. **Screen-reader no-op** (§2 P0) — `LocalPlatformScreenReader` defaults to inactive 3. **Screen-reader no-op** (§2 P0) - `LocalPlatformScreenReader` defaults to inactive
instead of throwing. instead of throwing.
4. **SDL slimming** (§3) — 10 unused subsystems disabled in `build-all.py`; 6 now-dead 4. **SDL slimming** (§3) - 10 unused subsystems disabled in `build-all.py`; 6 now-dead
macOS frameworks dropped from `sdl3.def`. Rebuilt; demo + apidemo link, demo runs. macOS frameworks dropped from `sdl3.def`. Rebuilt; demo + apidemo link, demo runs.
5. **Context menu reconciled** (§2 P0) — confirmed already working via the vendored legacy 5. **Context menu reconciled** (§2 P0) - confirmed already working via the vendored legacy
path; the 3 "NOP" seams are vestigial (disabled new API) — misleading TODOs corrected. path; the 3 "NOP" seams are vestigial (disabled new API) - misleading TODOs corrected.
6. **`CDN_TEXT_METRICS` diagnostic** (§1b) — env-gated line-metrics dump; macOS baseline 6. **`CDN_TEXT_METRICS` diagnostic** (§1b) - env-gated line-metrics dump; macOS baseline
captured, hypothesis sharpened (fork likely drops lineHeight leading). captured, hypothesis sharpened (fork likely drops lineHeight leading).
7. **Doc hygiene** (§5) — removed the stale historical `RENDERER.md` + 7. **Doc hygiene** (§5) - removed the stale historical `RENDERER.md` +
`SKIKO-MINGW-FEASIBILITY.md` (renderer essentials folded into `CLAUDE.md`; rest in git `SKIKO-MINGW-FEASIBILITY.md` (renderer essentials folded into `CLAUDE.md`; rest in git
history), fixed the `CLAUDE.md` typo + all dead doc links; extracted the fork history), fixed the `CLAUDE.md` typo + all dead doc links; extracted the fork
FreeType-scaler lead into §1c before deleting. Vendor drift verified clean. FreeType-scaler lead into §1c before deleting. Vendor drift verified clean.
**Windows pass (2026-08-04, this host — DPR 1).** On a real Windows host at last: (8) tab fix **Windows pass (2026-08-04, this host - DPR 1).** On a real Windows host at last: (8) tab fix
**CONFIRMED on-device** (§1a); (9) vertical metrics **RESOLVED** — native `--metricsprobe` heights **CONFIRMED on-device** (§1a); (9) vertical metrics **RESOLVED** - native `--metricsprobe` heights
== JVM `--metrics` exactly, feared "dropped leading" DISPROVEN (§1b); (10) fork `LineMetrics. == JVM `--metrics` exactly, feared "dropped leading" DISPROVEN (§1b); (10) fork `LineMetrics.
ascent/descent` mis-decode **FIXED in shared Kotlin** (`SkiaParagraphOps.lineMetrics()`, guarded ascent/descent` mis-decode **FIXED in shared Kotlin** (`SkiaParagraphOps.lineMetrics()`, guarded
no-op on official skiko) — repairs Windows caret/selection; (11) fork FontMgr/ICU **AUDITED** — CJK no-op on official skiko) - repairs Windows caret/selection; (11) fork FontMgr/ICU **AUDITED** - CJK
+ color-emoji + control-char fallback all work → the "empty FreeType stub" fear DISPROVEN (§1c); + color-emoji + control-char fallback all work → the "empty FreeType stub" fear DISPROVEN (§1c);
(12) `FontRasterizationSettings.native.kt` verified to mirror upstream VERBATIM + given a (12) `FontRasterizationSettings.native.kt` verified to mirror upstream VERBATIM + given a
`VENDOR-BASE` provenance line; rasterization stays upstream-faithful per-OS (user decision). Net: `VENDOR-BASE` provenance line; rasterization stays upstream-faithful per-OS (user decision). Net:
the fork's Skia matches upstream — the only deltas are its thin extern-C bindings, fixed in shared the fork's Skia matches upstream - the only deltas are its thin extern-C bindings, fixed in shared
Kotlin, **zero fork rebuild**. Kotlin, **zero fork rebuild**.
**Still not done — WIN-SMOKE interactive + release** (§5): interactive caret-height check (the one **Still not done - WIN-SMOKE interactive + release** (§5): interactive caret-height check (the one
behavioral consequence of the LineMetrics fix), the `PrintWindow` probe, `compileCommonMainKotlin behavioral consequence of the LineMetrics fix), the `PrintWindow` probe, `compileCommonMainKotlin
Metadata`, apiDump + publish FROM Windows, and the Compose 1.12.0-stable re-pin. **Deliberately Metadata`, apiDump + publish FROM Windows, and the Compose 1.12.0-stable re-pin. **Deliberately
deferred** (large/structural, rationale in-section): RTL, `PlatformFontLoader`, grapheme source-set deferred** (large/structural, rationale in-section): RTL, `PlatformFontLoader`, grapheme source-set
move, date CLDR, brush/gradient text — plus the big **vendor-the-upstream-skiko-text-engine move, date CLDR, brush/gradient text - plus the big **vendor-the-upstream-skiko-text-engine
refactor (§6)**, sequenced after these fixes land. refactor (§6)**, sequenced after these fixes land.
--- ---
## ▶ Continuing on Windows (mingwX64) — START HERE ## ▶ Continuing on Windows (mingwX64) - START HERE
**Read this before doing anything.** This file is the single source of truth for the road to **Read this before doing anything.** This file is the single source of truth for the road to
1.0.0. Everything in **"Landed toward 1.0.0"** above is **already implemented and committed on 1.0.0. Everything in **"Landed toward 1.0.0"** above is **already implemented and committed on
`main`** — do NOT redo it, just verify it on this host. Every open `[ ]` / `[~]` below is `main`** - do NOT redo it, just verify it on this host. Every open `[ ]` / `[~]` below is
remaining work. The macOS/Linux side is done and verified; the remaining blockers need a **real remaining work. The macOS/Linux side is done and verified; the remaining blockers need a **real
Windows host** — the mingwX64 build cross-compiled from mac/Linux fails at cinterop, and the Windows host** - the mingwX64 build cross-compiled from mac/Linux fails at cinterop, and the
bitsycore skiko **fork** can only be rebuilt on Windows. bitsycore skiko **fork** can only be rebuilt on Windows.
**The Windows fidelity reference is skiko-on-JVM Compose Desktop on THIS Windows host** **The Windows fidelity reference is skiko-on-JVM Compose Desktop on THIS Windows host**
(`gradlew :demo:run` — also DirectWrite/system fonts), NOT macOS. mac↔Windows metric deltas are (`gradlew :demo:run` - also DirectWrite/system fonts), NOT macOS. mac↔Windows metric deltas are
expected in upstream Compose Desktop too, so "matches Windows-JVM" is the acceptance bar, not expected in upstream Compose Desktop too, so "matches Windows-JVM" is the acceptance bar, not
"matches macOS". "matches macOS".
@@ -103,27 +103,27 @@ gradlew :apidemo:run :: JVM apidemo reference
``` ```
- The bridge plugin auto-drops `skiko-windows-x64.dll` next to the exe (`installWindowsSkiaDll`). - The bridge plugin auto-drops `skiko-windows-x64.dll` next to the exe (`installWindowsSkiaDll`).
Fork coords `com.bitsycore.skiko:skiko:0.150.1-mingw.1` (override `-PskikoMingwVersion`). Fork coords `com.bitsycore.skiko:skiko:0.150.1-mingw.2` (override `-PskikoMingwVersion`).
- `:demo` CLI flags (native AND jvm): `--screen=<Name>`, `--screenshot=<path.bmp>` (capture at - `:demo` CLI flags (native AND jvm): `--screen=<Name>`, `--screenshot=<path.bmp>` (capture at
quiescence then quit), `--gpu=auto|software|skia.opengl`. `CDN_TEXT_METRICS=1` env var dumps quiescence then quit), `--gpu=auto|software|skia.opengl`. `CDN_TEXT_METRICS=1` env var dumps
text metrics (§1b). `CDN_PROFILE=1` frame profiler. `:apidemo` has NO screenshot CLI — drive it text metrics (§1b). `CDN_PROFILE=1` frame profiler. `:apidemo` has NO screenshot CLI - drive it
by hand for the button-metrics visual. by hand for the button-metrics visual.
- If an IC-cache error appears after any module churn: delete `demo\build\kotlin-native-ic-cache`. - If an IC-cache error appears after any module churn: delete `demo\build\kotlin-native-ic-cache`.
### ✅ Windows session results (2026-08-04, this host — DPR 1.0) ### ✅ Windows session results (2026-08-04, this host - DPR 1.0)
Items 1–3 below are **DONE + verified on the shipped mingwX64 demo binary**; findings folded into Items 1–3 below are **DONE + verified on the shipped mingwX64 demo binary**; findings folded into
§1a/§1b/§1c. Only 4–5 (WIN-SMOKE interactive gate + apiDump/publish) remain. TL;DR: §1a/§1b/§1c. Only 4–5 (WIN-SMOKE interactive gate + apiDump/publish) remain. TL;DR:
- **§1a tab fix — CONFIRMED.** `--screen=BasicText` with `"start>a\tb<end\tcols\tand\tthere"` - **§1a tab fix - CONFIRMED.** `--screen=BasicText` with `"start>a\tb<end\tcols\tand\tthere"`
rendered `start>a b<end cols and there` — every `\t` a SPACE, zero `.notdef`. (Structural proof rendered `start>a b<end cols and there` - every `\t` a SPACE, zero `.notdef`. (Structural proof
too: the whole module has exactly 2 `.addText(` sites, both `shapedText`.) too: the whole module has exactly 2 `.addText(` sites, both `shapedText`.)
- **§1b vertical metrics — RESOLVED, the feared bug is DISPROVEN.** Native `--metricsprobe` vs JVM - **§1b vertical metrics - RESOLVED, the feared bug is DISPROVEN.** Native `--metricsprobe` vs JVM
`--metrics` (same host, NotoSans, density 1): **every paragraph height + non-M3 baseline matches `--metrics` (same host, NotoSans, density 1): **every paragraph height + non-M3 baseline matches
JVM EXACTLY** (size 11→24). Leading is NOT dropped. Two residual deltas found (details in §1b): JVM EXACTLY** (size 11→24). Leading is NOT dropped. Two residual deltas found (details in §1b):
(a) the fork's `LineMetrics.ascent/descent` are mis-decoded — **fixed in Kotlin** this session; (a) the fork's `LineMetrics.ascent/descent` are mis-decoded - **fixed in Kotlin** this session;
(b) M3 baseline drifts ≤0.77px (halfLeading, a shared-engine omission on ALL platforms, sub-pixel). (b) M3 baseline drifts ≤0.77px (halfLeading, a shared-engine omission on ALL platforms, sub-pixel).
- **§1c FontMgr/ICU — the "empty FreeType stub" fear is DISPROVEN.** Native render of CJK - **§1c FontMgr/ICU - the "empty FreeType stub" fear is DISPROVEN.** Native render of CJK
(你好世界 日本語 한국어) + **full-COLOR emoji** (😀🎉🚀❤) + control chars (nbsp/zwsp/en-dash) = zero (你好世界 日本語 한국어) + **full-COLOR emoji** (😀🎉🚀❤) + control chars (nbsp/zwsp/en-dash) = zero
tofu. Color emoji + CJK fallback prove `FontMgr.default` is a real DirectWrite-backed system tofu. Color emoji + CJK fallback prove `FontMgr.default` is a real DirectWrite-backed system
manager, not an empty custom FreeType one. Remaining §1c work is doc-only (capture `args.gn`). manager, not an empty custom FreeType one. Remaining §1c work is doc-only (capture `args.gn`).
@@ -138,18 +138,18 @@ set CDN_TEXT_METRICS=1 && ...demo.exe --screen=Buttons :: raw per-pa
### Ordered Windows work (do top-to-bottom; full detail in the numbered sections below) ### Ordered Windows work (do top-to-bottom; full detail in the numbered sections below)
1. **[§1a] Confirm the tab fix — ✅ DONE** (see session results above). Rendered a literal-tab 1. **[§1a] Confirm the tab fix - ✅ DONE** (see session results above). Rendered a literal-tab
string on the fork; all `\t`→space, no tofu. The `SkiaParagraphEngine.shapedText` normalization string on the fork; all `\t`→space, no tofu. The `SkiaParagraphEngine.shapedText` normalization
holds; no bypass path exists (2 `.addText(` sites, both `shapedText`). holds; no bypass path exists (2 `.addText(` sites, both `shapedText`).
2. **[§1b] Confirm/resolve vertical metrics — ✅ RESOLVED (feared bug DISPROVEN).** Used the 2. **[§1b] Confirm/resolve vertical metrics - ✅ RESOLVED (feared bug DISPROVEN).** Used the
`--metricsprobe` (native) vs `--metrics` (JVM) pair, NOT the screenshot diff (the JVM leg has no `--metricsprobe` (native) vs `--metrics` (JVM) pair, NOT the screenshot diff (the JVM leg has no
`--screenshot=<file>`; it dumps the same `metrics:` table). Every paragraph HEIGHT matches JVM `--screenshot=<file>`; it dumps the same `metrics:` table). Every paragraph HEIGHT matches JVM
exactly (leading applied identically) → correct-by-reference. Only a ≤0.77px M3-baseline drift exactly (leading applied identically) → correct-by-reference. Only a ≤0.77px M3-baseline drift
remains (halfLeading, all platforms). Separately fixed the fork's broken `LineMetrics.ascent/ remains (halfLeading, all platforms). Separately fixed the fork's broken `LineMetrics.ascent/
descent` in Kotlin. Full data in §1b. descent` in Kotlin. Full data in §1b.
3. **[§1c] Audit the fork FontMgr / gamma / ICU — ✅ AUDITED (fallback WORKS).** CJK + color emoji + 3. **[§1c] Audit the fork FontMgr / gamma / ICU - ✅ AUDITED (fallback WORKS).** CJK + color emoji +
control chars all render with zero tofu on the fork → `FontMgr.default` is a real system control chars all render with zero tofu on the fork → `FontMgr.default` is a real system
(DirectWrite) manager with working glyph fallback, NOT an empty FreeType stub. Remaining: capture (DirectWrite) manager with working glyph fallback, NOT an empty FreeType stub. Remaining: capture
the fork's REAL `args.gn` (`SK_GAMMA_*`, ICU packaging, freetype-vs-dwrite) into the fork repo so the fork's REAL `args.gn` (`SK_GAMMA_*`, ICU packaging, freetype-vs-dwrite) into the fork repo so
@@ -167,20 +167,20 @@ set CDN_TEXT_METRICS=1 && ...demo.exe --screen=Buttons :: raw per-pa
## 1. Cross-platform FIDELITY (headline) ## 1. Cross-platform FIDELITY (headline)
All Windows divergences live **below Kotlin** — the mingw source set reuses All Windows divergences live **below Kotlin** - the mingw source set reuses
`src/skikoRendererMain/kotlin` byte-for-byte (`compose/ui/ui/build.gradle.kts:102-113`), `src/skikoRendererMain/kotlin` byte-for-byte (`compose/ui/ui/build.gradle.kts:102-113`),
the only mingw-unique file is `PlatformGpu.mingw.kt` (13 lines, GPU only). So every the only mingw-unique file is `PlatformGpu.mingw.kt` (13 lines, GPU only). So every
divergence is in the **fork's Skia binary**, in **`FontMgr.default`'s per-OS impl**, or in divergence is in the **fork's Skia binary**, in **`FontMgr.default`'s per-OS impl**, or in
the **GPU backend**. The fork is external (`bitsycore/skiko`, the **GPU backend**. The fork is external (`bitsycore/skiko`,
`com.bitsycore.skiko:skiko:0.150.1-mingw.1`) and its GN args are **not verifiable from this `com.bitsycore.skiko:skiko:0.150.1-mingw.2`) and its GN args are **not verifiable from this
tree** — recovering them is a prerequisite for several fixes below. tree** - recovering them is a prerequisite for several fixes below.
**Fidelity reference for Windows is skiko-on-JVM Compose Desktop on Windows** (also **Fidelity reference for Windows is skiko-on-JVM Compose Desktop on Windows** (also
DirectWrite), NOT macOS/Linux. macOS↔Windows metric deltas partly exist in upstream DirectWrite), NOT macOS/Linux. macOS↔Windows metric deltas partly exist in upstream
Compose Desktop too (per-platform font backends). Reframe V-items as "match JVM-Windows" Compose Desktop too (per-platform font backends). Reframe V-items as "match JVM-Windows"
acceptance tests, not "bugs vs macOS". acceptance tests, not "bugs vs macOS".
### 1a. Windows tab-character tofu — HIGH CONFIDENCE (root cause traced) ### 1a. Windows tab-character tofu - HIGH CONFIDENCE (root cause traced)
Kotlin sets `ParagraphStyle.replaceTabCharacters = true` Kotlin sets `ParagraphStyle.replaceTabCharacters = true`
(`compose/ui/ui-text/src/skikoRendererMain/kotlin/.../renderer/skia/SkiaParagraphEngine.kt:234-236`, (`compose/ui/ui-text/src/skikoRendererMain/kotlin/.../renderer/skia/SkiaParagraphEngine.kt:234-236`,
@@ -191,12 +191,12 @@ On Windows the same call runs against the fork's flat extern-C DLL, which eviden
box. Font fallback is a **red herring**: no font has a tab glyph, only tab→space box. Font fallback is a **red herring**: no font has a tab glyph, only tab→space
replacement cures it. replacement cures it.
- [x] **P0** Confirm — ✅ DONE (Windows 2026-08-04): rendered a literal-tab string on the fork - [x] **P0** Confirm - ✅ DONE (Windows 2026-08-04): rendered a literal-tab string on the fork
(`--screen=BasicText`); every `\t` shows a SPACE, no `.notdef`. The fork renders everything (`--screen=BasicText`); every `\t` shows a SPACE, no `.notdef`. The fork renders everything
else clean too, so tab→space was the only gap. Structural backstop: the module has exactly 2 else clean too, so tab→space was the only gap. Structural backstop: the module has exactly 2
`.addText(` sites, both `shapedText` — no raw tab can reach skiko. `.addText(` sites, both `shapedText` - no raw tab can reach skiko.
- [x] **P0** Fix (ship now, author project code): in - [x] **P0** Fix (ship now, author project code): in
`.../SkiaParagraphEngine.kt`, feed skiko a normalized copy — added `.../SkiaParagraphEngine.kt`, feed skiko a normalized copy - added
`private val shapedText = if (text.indexOf('\t') >= 0) text.replace('\t', ' ') else text`, `private val shapedText = if (text.indexOf('\t') >= 0) text.replace('\t', ' ') else text`,
used at `addText` (no-span path) and via `shapedText.substring(segStart, segEnd)` used at `addText` (no-span path) and via `shapedText.substring(segStart, segEnd)`
(span path); `text` kept for ALL length/index queries (`getRectsForRange`, (span path); `text` kept for ALL length/index queries (`getRectsForRange`,
@@ -204,21 +204,21 @@ replacement cures it.
every offset consumer stays correct; this is exactly what Skia's flag does internally → every offset consumer stays correct; this is exactly what Skia's flag does internally →
upstream-faithful; makes behavior platform-independent (no fork rebuild needed). upstream-faithful; makes behavior platform-independent (no fork rebuild needed).
Kept `replaceTabCharacters = true` (harmless/redundant elsewhere). Kept `replaceTabCharacters = true` (harmless/redundant elsewhere).
**DONE** — compiles clean on macosArm64. Windows tofu render still needs on-device **DONE** - compiles clean on macosArm64. Windows tofu render still needs on-device
confirmation (WIN-SMOKE §5). confirmation (WIN-SMOKE §5).
- [ ] **P2** Long-term true fix (fork, out of tree): wire - [ ] **P2** Long-term true fix (fork, out of tree): wire
`ParagraphStyle::setReplaceTabCharacters` through the fork's extern-C surface, `ParagraphStyle::setReplaceTabCharacters` through the fork's extern-C surface,
rebuild/republish `skiko-windows-x64.dll`, bump `-PskikoMingwVersion`. Do this *in rebuild/republish `skiko-windows-x64.dll`, bump `-PskikoMingwVersion`. Do this *in
addition to* the Kotlin fix, not instead — it keeps the port thin. Can't be verified addition to* the Kotlin fix, not instead - it keeps the port thin. Can't be verified
in-tree. in-tree.
### 1b. macOS-vs-Windows vertical text metrics — ✅ RESOLVED (Windows 2026-08-04): heights match JVM, feared bug DISPROVEN ### 1b. macOS-vs-Windows vertical text metrics - ✅ RESOLVED (Windows 2026-08-04): heights match JVM, feared bug DISPROVEN
**WINDOWS RESULT (2026-08-04).** All targets render through the SAME Skia, so metrics are expected **WINDOWS RESULT (2026-08-04).** All targets render through the SAME Skia, so metrics are expected
to be similar — and they are. Native `--metricsprobe` (fork) vs JVM `--metrics` (upstream skiko) on to be similar - and they are. Native `--metricsprobe` (fork) vs JVM `--metrics` (upstream skiko) on
this host (NotoSans, density 1) print IDENTICAL paragraph heights and non-M3 baselines for every this host (NotoSans, density 1) print IDENTICAL paragraph heights and non-M3 baselines for every
size 11→24 (`cell`/`one`/`three`/`oneM3`/`threeM3`, `base1`/`base3` all match). The fork does NOT size 11→24 (`cell`/`one`/`three`/`oneM3`/`threeM3`, `base1`/`base3` all match). The fork does NOT
drop lineHeight leading — the hypothesis below is disproven. Two small deltas remained, both drop lineHeight leading - the hypothesis below is disproven. Two small deltas remained, both
handled in SHARED Kotlin (no fork rebuild, keeping fork-reliance minimal): handled in SHARED Kotlin (no fork rebuild, keeping fork-reliance minimal):
1. **Fork `LineMetrics.ascent/descent` mis-decode → FIXED in Kotlin (this session).** 1. **Fork `LineMetrics.ascent/descent` mis-decode → FIXED in Kotlin (this session).**
@@ -229,7 +229,7 @@ handled in SHARED Kotlin (no fork rebuild, keeping fork-reliance minimal):
now rebuilds ascent/descent from the reliable baseline+height when they're internally now rebuilds ascent/descent from the reliable baseline+height when they're internally
inconsistent (guard `|ascent-(baseline-lineTop)|>0.5` = no-op on official skiko). Repairs caret inconsistent (guard `|ascent-(baseline-lineTop)|>0.5` = no-op on official skiko). Repairs caret
height / getLineTop/Bottom / vertical hit-test on Windows. **Behavioral caret check → WIN-SMOKE.** height / getLineTop/Bottom / vertical hit-test on Windows. **Behavioral caret check → WIN-SMOKE.**
2. **M3 baseline drift ≤0.77px (halfLeading) — SHARED-engine omission, all platforms.** `base1M3` 2. **M3 baseline drift ≤0.77px (halfLeading) - SHARED-engine omission, all platforms.** `base1M3`
differs native-vs-JVM by ≤0.77px in the 11→24 sweep (size 14 → 0.28px, sub-pixel), 2.19px in differs native-vs-JVM by ≤0.77px in the 11→24 sweep (size 14 → 0.28px, sub-pixel), 2.19px in
the contrived `boundary 24/25` case. Root cause is NOT the fork: `SkiaParagraphEngine` maps the contrived `boundary 24/25` case. Root cause is NOT the fork: `SkiaParagraphEngine` maps
`LineHeightStyle.trim`→HeightMode but never wires `halfLeading`/`LineHeightStyle.Alignment`, so `LineHeightStyle.trim`→HeightMode but never wires `halfLeading`/`LineHeightStyle.Alignment`, so
@@ -237,7 +237,7 @@ handled in SHARED Kotlin (no fork rebuild, keeping fork-reliance minimal):
shared Kotlin (`textStyle.halfLeading` from the alignment); deferred pending a cross-platform shared Kotlin (`textStyle.halfLeading` from the alignment); deferred pending a cross-platform
parity re-check (can't run mac parity from a Windows host). parity re-check (can't run mac parity from a Windows host).
Native-fork metricsprobe vs JVM `--metrics` (this host) — identical except `base?M3`: Native-fork metricsprobe vs JVM `--metrics` (this host) - identical except `base?M3`:
``` ```
size=14 lh=20 cell=19 one=19 three=59 base1=14.898 base3=54.898 oneM3=20 threeM3=60 base1M3: 15.682 fork / 15.398 jvm size=14 lh=20 cell=19 one=19 three=59 base1=14.898 base3=54.898 oneM3=20 threeM3=60 base1M3: 15.682 fork / 15.398 jvm
size=24 lh=30 cell=33 one=33 three=93 base1=25.968 base3=85.968 oneM3=30 threeM3=90 base1M3: 23.546 fork / 24.312 jvm size=24 lh=30 cell=33 one=33 three=93 base1=25.968 base3=85.968 oneM3=30 threeM3=90 base1M3: 23.546 fork / 24.312 jvm
@@ -253,7 +253,7 @@ of the font itself (`SkiaParagraphEngine.kt:244-249`, matches upstream
reported ascent/descent. reported ascent/descent.
Root cause: **`FontMgr.default` resolves to a different Skia font scaler per build** for Root cause: **`FontMgr.default` resolves to a different Skia font scaler per build** for
the *same* `font/NotoSans.ttf` bytes (`SkiaFonts.kt:38,44,50`) — CoreText on macOS, the *same* `font/NotoSans.ttf` bytes (`SkiaFonts.kt:38,44,50`) - CoreText on macOS,
fontconfig/FreeType on Linux, DirectWrite (or FreeType, unverified) in the fork on Windows. fontconfig/FreeType on Linux, DirectWrite (or FreeType, unverified) in the fork on Windows.
Different backends select different metric tables (hhea `ascender/descender` vs OS/2 Different backends select different metric tables (hhea `ascender/descender` vs OS/2
`sTypoAscender/Descender` vs `usWinAscent/Descent`) and honor the OS/2 `USE_TYPO_METRICS` `sTypoAscender/Descender` vs `usWinAscent/Descent`) and honor the OS/2 `USE_TYPO_METRICS`
@@ -267,7 +267,7 @@ ignores it) and V3 (subpixel/edging) are secondary and per-scaler.
Windows-JVM*. If it matches JVM-Windows it's correct-by-reference (not a bug), and the Windows-JVM*. If it matches JVM-Windows it's correct-by-reference (not a bug), and the
macOS↔Windows delta is expected upstream behavior. macOS↔Windows delta is expected upstream behavior.
- [x] **P0** Confirm FIRST — ✅ RESOLVED (Windows 2026-08-04; heights == JVM, see WINDOWS RESULT - [x] **P0** Confirm FIRST - ✅ RESOLVED (Windows 2026-08-04; heights == JVM, see WINDOWS RESULT
above). Diagnostic **LANDED**: `SkiaParagraphEngine.kt` dumps above). Diagnostic **LANDED**: `SkiaParagraphEngine.kt` dumps
`defaultFont.metrics.{ascent,descent,leading}`, `paragraph.height`, `defaultFont.metrics.{ascent,descent,leading}`, `paragraph.height`,
`lineMetrics[0].{ascent,descent,baseline,height}`, `familyName` + `fontPx` per built `lineMetrics[0].{ascent,descent,baseline,height}`, `familyName` + `fontPx` per built
@@ -283,9 +283,9 @@ macOS↔Windows delta is expected upstream behavior.
labels ("Filled Button" / "OutlinedButton" / "TextButton"): font metrics labels ("Filled Button" / "OutlinedButton" / "TextButton"): font metrics
`ascent=-32.07 descent=8.79 leading=0` (font box = **40.86px**), but the line box is `ascent=-32.07 descent=8.79 leading=0` (font box = **40.86px**), but the line box is
`line0[ascent=37.67 descent=10.33 height=48.0]`, `paraHeight=48`. So macOS DOES apply `line0[ascent=37.67 descent=10.33 height=48.0]`, `paraHeight=48`. So macOS DOES apply
the lineHeight leading (48 > 40.86) and distributes it **asymmetrically — ~5.6px above, the lineHeight leading (48 > 40.86) and distributes it **asymmetrically - ~5.6px above,
~1.5px below** → exactly the "extra space on top of the text" the report describes. ~1.5px below** → exactly the "extra space on top of the text" the report describes.
**SHARPENED HYPOTHESIS:** this is NOT a mac bug — it's correct lineHeight distribution. **SHARPENED HYPOTHESIS:** this is NOT a mac bug - it's correct lineHeight distribution.
The likely Windows story is the MIRROR of §1a: the fork's flat extern-C surface doesn't The likely Windows story is the MIRROR of §1a: the fork's flat extern-C surface doesn't
wire the `ParagraphStyle` height/strut path, so Windows renders **tight to the font box wire the `ParagraphStyle` height/strut path, so Windows renders **tight to the font box
(~41px, no leading)** = the "fitted to text" look. **Sharpened WIN-SMOKE check:** for the (~41px, no leading)** = the "fitted to text" look. **Sharpened WIN-SMOKE check:** for the
@@ -294,7 +294,7 @@ macOS↔Windows delta is expected upstream behavior.
`replaceTabCharacters`); a Kotlin fallback is a manual `StrutStyle`. If Windows-native `replaceTabCharacters`); a Kotlin fallback is a manual `StrutStyle`. If Windows-native
== Windows-JVM, it's correct-by-reference. == Windows-JVM, it's correct-by-reference.
**macOS baseline raw lines** (`CDN_TEXT_METRICS=1 demo --screen=Buttons`, DPR 2) — diff **macOS baseline raw lines** (`CDN_TEXT_METRICS=1 demo --screen=Buttons`, DPR 2) - diff
Windows-native + Windows-JVM against these; the fontPx=30 rows are the cleanest signal: Windows-native + Windows-JVM against these; the fontPx=30 rows are the cleanest signal:
``` ```
text='Filled Button' fontPx=30.0 ascent=-32.070007 descent=8.789978 leading=0.0 paraHeight=48.0 line0[ascent=37.674 descent=10.326 baseline=37.674 height=48.0] text='Filled Button' fontPx=30.0 ascent=-32.070007 descent=8.789978 leading=0.0 paraHeight=48.0 line0[ascent=37.674 descent=10.326 baseline=37.674 height=48.0]
@@ -305,7 +305,7 @@ macOS↔Windows delta is expected upstream behavior.
(family='Noto Sans' on all.) Expectation if correct: `fontPx`, `paraHeight`, `line0.height` (family='Noto Sans' on all.) Expectation if correct: `fontPx`, `paraHeight`, `line0.height`
match JVM-Windows exactly; the raw `ascent`/`descent` may differ from these macOS numbers match JVM-Windows exactly; the raw `ascent`/`descent` may differ from these macOS numbers
because the Windows scaler is FreeType (§1c), but the LINE BOX must still match JVM-Windows. because the Windows scaler is FreeType (§1c), but the LINE BOX must still match JVM-Windows.
- [x] **P1** ~~If Windows-native != Windows-JVM: fix in the fork~~ — **N/A (2026-08-04):** heights - [x] **P1** ~~If Windows-native != Windows-JVM: fix in the fork~~ - **N/A (2026-08-04):** heights
MATCH JVM-Windows exactly, so no fork scaler/FontMgr change is needed for vertical metrics. MATCH JVM-Windows exactly, so no fork scaler/FontMgr change is needed for vertical metrics.
The `makeFromData` typefaces already get the same metric-table selection as skiko-JVM-Windows. The `makeFromData` typefaces already get the same metric-table selection as skiko-JVM-Windows.
The one residual (M3 halfLeading) is a shared-Kotlin fix, not a fork one. The one residual (M3 halfLeading) is a shared-Kotlin fix, not a fork one.
@@ -313,20 +313,20 @@ macOS↔Windows delta is expected upstream behavior.
wanted over matching each host's stock Compose Desktop: build a single wanted over matching each host's stock Compose Desktop: build a single
FreeType/`SkFontMgr_Custom_Empty`-backed `SkFontMgr` used on ALL native targets for FreeType/`SkFontMgr_Custom_Empty`-backed `SkFontMgr` used on ALL native targets for
the bundled fonts, so NotoSans yields identical ascent/descent everywhere. the bundled fonts, so NotoSans yields identical ascent/descent everywhere.
Intentional departure from per-platform upstream behavior — decide deliberately. Intentional departure from per-platform upstream behavior - decide deliberately.
- [ ] **DO NOT** "fix" by injecting a default `LineHeightStyle` or clamping ascent in - [ ] **DO NOT** "fix" by injecting a default `LineHeightStyle` or clamping ascent in
`SkiaParagraphEngine.kt` — that desyncs from upstream `DISABLE_ALL` and masks, not `SkiaParagraphEngine.kt` - that desyncs from upstream `DISABLE_ALL` and masks, not
resolves, the metric divergence. resolves, the metric divergence.
### 1c. Other fork-vs-official divergences ### 1c. Other fork-vs-official divergences
- [x] **P1** ~~`FontMgr.default` may be an empty/stub manager on the fork~~ — **DISPROVEN - [x] **P1** ~~`FontMgr.default` may be an empty/stub manager on the fork~~ - **DISPROVEN
(Windows 2026-08-04).** Native render of CJK (你好世界 日本語 한국어) + **full-COLOR emoji** (Windows 2026-08-04).** Native render of CJK (你好世界 日本語 한국어) + **full-COLOR emoji**
(😀🎉🚀❤) + control chars = zero tofu. Color emoji + CJK fallback are impossible with an empty (😀🎉🚀❤) + control chars = zero tofu. Color emoji + CJK fallback are impossible with an empty
custom FreeType manager, so the fork's `FontMgr.default` IS a real DirectWrite-backed system custom FreeType manager, so the fork's `FontMgr.default` IS a real DirectWrite-backed system
manager with working glyph enumeration/fallback. No fork change needed. `SkiaFonts.kt` wiring manager with working glyph enumeration/fallback. No fork change needed. `SkiaFonts.kt` wiring
stays as-is. stays as-is.
- [x] **P1** `FontMgrWithFallback` — ANALYZED + fallback now VERIFIED on Windows. The port - [x] **P1** `FontMgrWithFallback` - ANALYZED + fallback now VERIFIED on Windows. The port
does `setDefaultFontManager(fontMgr)` [system] + `setAssetFontManager(provider)` does `setDefaultFontManager(fontMgr)` [system] + `setAssetFontManager(provider)`
[bundled] (`SkiaFonts.kt:49-52`). This is **functionally equivalent** for missing-glyph [bundled] (`SkiaFonts.kt:49-52`). This is **functionally equivalent** for missing-glyph
fallback on macOS/Linux/Windows-JVM: skiko's shaper tries the asset provider (bundled fallback on macOS/Linux/Windows-JVM: skiko's shaper tries the asset provider (bundled
@@ -335,15 +335,15 @@ macOS↔Windows delta is expected upstream behavior.
`setDefaultFontManager(FontMgrWithFallback(provider))` risks a macOS fallback `setDefaultFontManager(FontMgrWithFallback(provider))` risks a macOS fallback
regression with no observable gain, and I can't visually verify CJK/emoji fallback on regression with no observable gain, and I can't visually verify CJK/emoji fallback on
this host. **The real gap was thought to be Windows-fork-only** (fork `FontMgr.default` this host. **The real gap was thought to be Windows-fork-only** (fork `FontMgr.default`
empty stub → no system fallback) — but that's **DISPROVEN (2026-08-04):** CJK + color-emoji empty stub → no system fallback) - but that's **DISPROVEN (2026-08-04):** CJK + color-emoji
fallback works on native Windows too (item above). So the setDefault/setAsset wiring is fine fallback works on native Windows too (item above). So the setDefault/setAsset wiring is fine
on ALL targets. **Decision:** leave the Kotlin as-is; nothing to fix in the fork here. Re-open on ALL targets. **Decision:** leave the Kotlin as-is; nothing to fix in the fork here. Re-open
only if a CJK/emoji fallback bug is actually observed. only if a CJK/emoji fallback bug is actually observed.
- [x] **P2** ~~ICU/unicode data packaged differently in the fork~~ — **VERIFIED OK (2026-08-04).** - [x] **P2** ~~ICU/unicode data packaged differently in the fork~~ - **VERIFIED OK (2026-08-04).**
Rendered U+00A0 (nbsp → visible space), U+200B (zwsp → zero-width/collapsed), en-dash, and Rendered U+00A0 (nbsp → visible space), U+200B (zwsp → zero-width/collapsed), en-dash, and
U+0009 (tab, pre-replacement) on native Windows: all classified/handled correctly, no tofu. U+0009 (tab, pre-replacement) on native Windows: all classified/handled correctly, no tofu.
The force-exported `uloc_*_skiko` symbols do their job; skunicode classification works. (This The force-exported `uloc_*_skiko` symbols do their job; skunicode classification works. (This
is also why HarfBuzz shaping + skparagraph bidi work at all — ICU data is present via is also why HarfBuzz shaping + skparagraph bidi work at all - ICU data is present via
icudtl.dat.) icudtl.dat.)
- [~] **P2** Text gamma/AA edges: a Skia **build-time constant** - [~] **P2** Text gamma/AA edges: a Skia **build-time constant**
(`SK_GAMMA_EXPONENT`/`SK_GAMMA_CONTRAST`/`SK_GAMMA_APPLY_TO_A8`), NOT a GL-vs-Metal (`SK_GAMMA_EXPONENT`/`SK_GAMMA_CONTRAST`/`SK_GAMMA_APPLY_TO_A8`), NOT a GL-vs-Metal
@@ -355,25 +355,25 @@ macOS↔Windows delta is expected upstream behavior.
upstream Compose Desktop's per-OS defaults VERBATIM (Win/mac=Normal hinting, Linux=Slight). upstream Compose Desktop's per-OS defaults VERBATIM (Win/mac=Normal hinting, Linux=Slight).
**User decision:** keep the upstream-faithful per-OS behavior (match each platform's stock **User decision:** keep the upstream-faithful per-OS behavior (match each platform's stock
CMP), NOT a forced uniform cross-platform look. So there is no SDL_TTF-era gamma hack left to CMP), NOT a forced uniform cross-platform look. So there is no SDL_TTF-era gamma hack left to
strip — Skia is configured exactly as upstream does it. strip - Skia is configured exactly as upstream does it.
- [x] **GL-vs-Metal AA/gamma/color-space — RULED OUT as a primary cause.** Both bridges - [x] **GL-vs-Metal AA/gamma/color-space - RULED OUT as a primary cause.** Both bridges
create the surface `colorSpace = null` (`SkiaGLBridge.kt:68-74`, create the surface `colorSpace = null` (`SkiaGLBridge.kt:68-74`,
`SkiaMetalBridge.kt:133-139`) — identical un-color-managed legacy blending. Only `SkiaMetalBridge.kt:133-139`) - identical un-color-managed legacy blending. Only
deltas are cosmetic-correct: `RGBA_8888`/`BOTTOM_LEFT` (GL) vs `BGRA_8888`/`TOP_LEFT` deltas are cosmetic-correct: `RGBA_8888`/`BOTTOM_LEFT` (GL) vs `BGRA_8888`/`TOP_LEFT`
(Metal) — channel order + Y-flip, matched to buffers. Windows uses OpenGL (Metal) - channel order + Y-flip, matched to buffers. Windows uses OpenGL
(`PlatformGpu.kt:20`), same backend as Linux, so GL-vs-Metal can't explain a (`PlatformGpu.kt:20`), same backend as Linux, so GL-vs-Metal can't explain a
Windows-vs-Linux gap at all. Deprioritized — no action. Windows-vs-Linux gap at all. Deprioritized - no action.
- [~] **P1** The fork's Skia build config (GN args). **Lead extracted from git history** - [~] **P1** The fork's Skia build config (GN args). **Lead extracted from git history**
(the old `SKIKO-MINGW-FEASIBILITY.md`, deleted as stale — its build recipe lives in (the old `SKIKO-MINGW-FEASIBILITY.md`, deleted as stale - its build recipe lives in
`git show bdb5c64d^:SKIKO-MINGW-FEASIBILITY.md`): the fork's Route-1a recipe builds Skia `git show bdb5c64d^:SKIKO-MINGW-FEASIBILITY.md`): the fork's Route-1a recipe builds Skia
with **`skia_use_freetype`**, i.e. the Windows text scaler is **FreeType**, not with **`skia_use_freetype`**, i.e. the Windows text scaler is **FreeType**, not
DirectWrite and not macOS CoreText — a concrete reason the SAME NotoSans bytes yield DirectWrite and not macOS CoreText - a concrete reason the SAME NotoSans bytes yield
different ascent/descent on Windows (different metric-table selection) AND why fork different ascent/descent on Windows (different metric-table selection) AND why fork
`FontMgr.default` may not enumerate Windows system fonts for glyph fallback (FreeType `FontMgr.default` may not enumerate Windows system fonts for glyph fallback (FreeType
has no system fontmgr without fontconfig). **CAVEAT:** that's the *recommended recipe*, has no system fontmgr without fontconfig). **CAVEAT:** that's the *recommended recipe*,
not a verified dump of the shipped `0.150.1-mingw.1` GN args. **REMAINING:** whoever not a verified dump of the shipped `0.150.1-mingw.2` GN args. **REMAINING:** whoever
rebuilds the fork should capture the real `args.gn` (`SK_GAMMA_*`, ICU packaging, rebuilds the fork should capture the real `args.gn` (`SK_GAMMA_*`, ICU packaging,
freetype-vs-dwrite) alongside the fork sources so drift is auditable — NOT as a doc in freetype-vs-dwrite) alongside the fork sources so drift is auditable - NOT as a doc in
this repo (kept lean); a comment in the fork repo or `build-sdl.properties`-style pin. this repo (kept lean); a comment in the fork repo or `build-sdl.properties`-style pin.
--- ---
@@ -385,12 +385,12 @@ match pin `v1.12.0-beta03+dev4483`; zero commonMain rule-1 violations (the 3 aut
`androidx.compose.*` files in commonMain are all provenance-tracked Rule-3 vendors). The `androidx.compose.*` files in commonMain are all provenance-tracked Rule-3 vendors). The
macosMain DIAGNOSTIC GAP families (`ui/ui` 12, `foundation` 23: `CoreTextField.macos.kt`, macosMain DIAGNOSTIC GAP families (`ui/ui` 12, `foundation` 23: `CoreTextField.macos.kt`,
`SelectionManager.macos.kt`, `PlatformClipboard.macos.kt`, …) are **correctly excluded** `SelectionManager.macos.kt`, `PlatformClipboard.macos.kt`, …) are **correctly excluded**
(AppKit/NSView; the port uses SDL) — keep them in gaps. **Do not spend 1.0.0 effort (AppKit/NSView; the port uses SDL) - keep them in gaps. **Do not spend 1.0.0 effort
re-vendoring.** The debt is **completing native-actual stubs**. re-vendoring.** The debt is **completing native-actual stubs**.
### P0 — blocks fidelity/correctness ### P0 - blocks fidelity/correctness
- [~] **P0** Text context menu (right-click copy/paste/select-all) — **RECONCILED: already - [~] **P0** Text context menu (right-click copy/paste/select-all) - **RECONCILED: already
works, no reimplementation.** Static trace confirmed the menu is wired end-to-end works, no reimplementation.** Static trace confirmed the menu is wired end-to-end
through the vendored LEGACY path: text-level `ContextMenuArea` (`ContextMenu.native.kt`) through the vendored LEGACY path: text-level `ContextMenuArea` (`ContextMenu.native.kt`)
→ `CommonContextMenuArea` (`vendor/common/.../text/CommonContextMenuArea.kt`) → (native → `CommonContextMenuArea` (`vendor/common/.../text/CommonContextMenuArea.kt`) → (native
@@ -399,25 +399,25 @@ re-vendoring.** The debt is **completing native-actual stubs**.
(`isSecondaryPressed`) → `ContextMenuPopup` → `Popup.native.kt` (hosted by (`isSecondaryPressed`) → `ContextMenuPopup` → `Popup.native.kt` (hosted by
`LocalPopupHost` at the app root). Items are real: `TextFieldSelectionManager` / `LocalPopupHost` at the app root). Items are real: `TextFieldSelectionManager` /
`SelectionManager.contextMenuBuilder` emit Cut/Copy/Paste/SelectAll with real actions. `SelectionManager.contextMenuBuilder` emit Cut/Copy/Paste/SelectAll with real actions.
SDL right-click → `PointerButton.Secondary` (`SDL3EventMapper.kt`) → SDL right-click → `PointerButton.Secondary` (`AppEvent.kt`) →
`isSecondaryPressed` (`PointerEventBridge`). The three "NOP" seams belong to the `isSecondaryPressed` (`PointerEventBridge`). The three "NOP" seams belong to the
DISABLED *new* context-menu API and are unreachable — **DONE:** their misleading DISABLED *new* context-menu API and are unreachable - **DONE:** their misleading
`TODO(CMP-7819)` comments corrected to say so, in all three files. `TODO(CMP-7819)` comments corrected to say so, in all three files.
**REMAINING:** an interactive right-click smoke check (no headless driver here — fold **REMAINING:** an interactive right-click smoke check (no headless driver here - fold
into manual/WIN-SMOKE). Known limitation, not a breakage: Paste enablement is into manual/WIN-SMOKE). Known limitation, not a breakage: Paste enablement is
plain-text-only (`ClipboardPasteState.hasClip = hasText`, see P2 below). plain-text-only (`ClipboardPasteState.hasClip = hasText`, see P2 below).
- [ ] **P1** DatePicker/TimePicker localization — VERIFIED current state: - [ ] **P1** DatePicker/TimePicker localization - VERIFIED current state:
`material3/.../internal/PlatformDateFormat.native.kt` is a real kotlinx-datetime `material3/.../internal/PlatformDateFormat.native.kt` is a real kotlinx-datetime
formatter that DOES honor CLDR patterns/skeletons (renders "Jul 29, 2026" / formatter that DOES honor CLDR patterns/skeletons (renders "Jul 29, 2026" /
"July 2026" correctly — the agent's "always yyyy-MM-dd" finding was stale). Genuine "July 2026" correctly - the agent's "always yyyy-MM-dd" finding was stale). Genuine
remaining gaps, all narrower than a correctness blocker: field NAMES English-only remaining gaps, all narrower than a correctness blocker: field NAMES English-only
(`:29 weekdayNames`, `:147-154 MONTH_NAMES/ABBR` — no CLDR data bundled), `:27 (`:29 weekdayNames`, `:147-154 MONTH_NAMES/ABBR` - no CLDR data bundled), `:27
firstDayOfWeek=1` fixed Sunday, `:67 is24HourFormat()=true` fixed, `:46 parse()` firstDayOfWeek=1` fixed Sunday, `:67 is24HourFormat()=true` fixed, `:46 parse()`
ISO-8601-only. Upstream `darwinMain` uses `NSDateFormatter` (not portable). **Fix ISO-8601-only. Upstream `darwinMain` uses `NSDateFormatter` (not portable). **Fix
(author):** bundle a CLDR subset or K/N i18n lib for localized names + locale-aware (author):** bundle a CLDR subset or K/N i18n lib for localized names + locale-aware
first-day/24h; also unblocks `CalendarLocale.native.kt:20` (fixed `"en"`). Ships first-day/24h; also unblocks `CalendarLocale.native.kt:20` (fixed `"en"`). Ships
readable English dates today — polish, not a P0 gate. readable English dates today - polish, not a P0 gate.
- [x] **P0** Float pointer coordinates — `SDL3EventMapper.kt` `.toInt()`-truncated SDL's - [x] **P0** Float pointer coordinates - `AppEvent.kt` `.toInt()`-truncated SDL's
Float `mb.x`/`mb.y`/`mm.x`/`mm.y` and the wheel `mouse_x/y` before the DPR multiply in Float `mb.x`/`mb.y`/`mm.x`/`mm.y` and the wheel `mouse_x/y` before the DPR multiply in
`ComposeWindow.kt`. On 2× displays a click at logical 100.9 → physical 200 not ~201, `ComposeWindow.kt`. On 2× displays a click at logical 100.9 → physical 200 not ~201,
quantizing the caret to 2px steps near glyph edges. **DONE:** widened quantizing the caret to 2px steps near glyph edges. **DONE:** widened
@@ -426,92 +426,92 @@ re-vendoring.** The debt is **completing native-actual stubs**.
`.toFloat()` at the two `ComposeWindow` read sites. Consumers fully contained (verified `.toFloat()` at the two `ComposeWindow` read sites. Consumers fully contained (verified
by grep). Builds + runs clean on macosArm64. by grep). Builds + runs clean on macosArm64.
### P1 — quality / parity ### P1 - quality / parity
- [ ] **P1** RTL text unsupported — `ui-text/.../SkiaParagraph.native.kt:112` - [ ] **P1** RTL text unsupported - `ui-text/.../SkiaParagraph.native.kt:112`
`textDirection = ResolvedTextDirection.Ltr` fixed field; `getParagraphDirection()` `textDirection = ResolvedTextDirection.Ltr` fixed field; `getParagraphDirection()`
(`:202`) always Ltr. Bidi run direction (`:204`) does read box direction (partial (`:202`) always Ltr. Bidi run direction (`:204`) does read box direction (partial
machinery). Root cause: `text/intl/Locale.native.kt isRtl()` unimplemented. Fix machinery). Root cause: `text/intl/Locale.native.kt isRtl()` unimplemented. Fix
(author) tied to P1.2 engine. (author) tied to P1.2 engine.
- [ ] **P1** `PlatformFontLoader` NOP — `ui-text/.../font/FontFamilyResolver.native.kt` - [ ] **P1** `PlatformFontLoader` NOP - `ui-text/.../font/FontFamilyResolver.native.kt`
`SdlPlatformFontLoader.loadBlocking`/`awaitLoad` NOP; `SdlPlatformFontLoader.loadBlocking`/`awaitLoad` NOP;
`PlatformFontFamilyTypefaceAdapter.resolve` returns `Immutable(Unit)`. Raw androidx `PlatformFontFamilyTypefaceAdapter.resolve` returns `Immutable(Unit)`. Raw androidx
`Font(bytes)`/`ResourceFont` don't load (compose-resources `Font()` works). Upstream `Font(bytes)`/`ResourceFont` don't load (compose-resources `Font()` works). Upstream
`SkiaFontLoader.skiko.kt` is the vendorable reference. This is the long-deferred P3.2 `SkiaFontLoader.skiko.kt` is the vendorable reference. This is the long-deferred P3.2
font resolver — **high-risk L**, parity harness wouldn't exercise it. Scope font resolver - **high-risk L**, parity harness wouldn't exercise it. Scope
deliberately for 1.0.0 (accept-and-document vs attempt). **NOW ALSO GATES §6** — the deliberately for 1.0.0 (accept-and-document vs attempt). **NOW ALSO GATES §6** - the
2026-08-04 spike found the verbatim engine-vendor routes font resolution through this NOP, so 2026-08-04 spike found the verbatim engine-vendor routes font resolution through this NOP, so
implementing this loader (so `SkiaFonts`' icon/variable-axis model rides upstream's seam) and implementing this loader (so `SkiaFonts`' icon/variable-axis model rides upstream's seam) and
the §6 engine-vendor are ONE effort. Do them together, with macOS/Linux verification. the §6 engine-vendor are ONE effort. Do them together, with macOS/Linux verification.
- [ ] **P1** `CharHelpers.skiko.kt` grapheme-break logic — the ONE cheap selective-vendor - [ ] **P1** `CharHelpers.skiko.kt` grapheme-break logic - the ONE cheap selective-vendor
win from the hand-rolled text engine. Fixes `findPrecedingBreak`/`findFollowingBreak` win from the hand-rolled text engine. Fixes `findPrecedingBreak`/`findFollowingBreak`
splitting emoji/combining marks in `StringHelpers.native.kt:31` + splitting emoji/combining marks in `StringHelpers.native.kt:31` +
`CharHelpers.native.kt:14`. (Giving `:foundation` a `skikoRenderer` source set would `CharHelpers.native.kt:14`. (Giving `:foundation` a `skikoRenderer` source set would
unblock this + `DragAndDropSource.skiko` — L-effort structural move, per §13a.) unblock this + `DragAndDropSource.skiko` - L-effort structural move, per §13a.)
- [~] **P1** `Serif`/`Cursive` generic families — RE-SCOPED after reading `SkiaFonts.kt`. - [~] **P1** `Serif`/`Cursive` generic families - RE-SCOPED after reading `SkiaFonts.kt`.
They do NOT silently collapse to sans: `baseTypeface()` routes `generic:serif` / They do NOT silently collapse to sans: `baseTypeface()` routes `generic:serif` /
`generic:cursive` through `resolveGeneric()` → `fontMgr.matchFamilyStyle()` over a `generic:cursive` through `resolveGeneric()` → `fontMgr.matchFamilyStyle()` over a
per-OS candidate list (`genericFamilyAliases`: Times/Noto Serif/… on each host), only per-OS candidate list (`genericFamilyAliases`: Times/Noto Serif/… on each host), only
falling back to the bundled default if NONE is installed. So serif/cursive already falling back to the bundled default if NONE is installed. So serif/cursive already
render as a real serif/cursive wherever the OS ships one (all three target OSes do). render as a real serif/cursive wherever the OS ships one (all three target OSes do).
The only true gap is **cross-platform pixel-identity** / a host with no serif installed, The only true gap is **cross-platform pixel-identity** / a host with no serif installed,
which needs a BUNDLED Noto Serif — an app-level opt-in (buildSrc `downloadNotoFonts` + which needs a BUNDLED Noto Serif - an app-level opt-in (buildSrc `downloadNotoFonts` +
data.kres size cost), not a library correctness bug. **Decision:** leave library data.kres size cost), not a library correctness bug. **Decision:** leave library
resolution as-is; offer bundled-serif as an opt-in later if an app needs it. resolution as-is; offer bundled-serif as an opt-in later if an app needs it.
- [x] **P1** Document the hand-rolled text engine as an **accepted architectural - [x] **P1** Document the hand-rolled text engine as an **accepted architectural
deviation** — **DONE:** rationale header added to the top of deviation** - **DONE:** rationale header added to the top of
`ui-text/compose-fork.txt` (the exact place someone would go to "fill" the gaps), `ui-text/compose-fork.txt` (the exact place someone would go to "fill" the gaps),
explaining the reduced local engine, the flat-source-set reason, and that RTL / explaining the reduced local engine, the flat-source-set reason, and that RTL /
stroke-DrawStyle / grapheme reductions are tracked here (§2), not fixed by vendoring. stroke-DrawStyle / grapheme reductions are tracked here (§2), not fixed by vendoring.
Also reflected in `SkiaParagraphEngine.kt`'s header and CLAUDE.md's renderer summary. Also reflected in `SkiaParagraphEngine.kt`'s header and CLAUDE.md's renderer summary.
### P1/P2 — reduced-coverage + accessibility ### P1/P2 - reduced-coverage + accessibility
- [ ] **P1** Text paint path handles only `SolidColor` - [ ] **P1** Text paint path handles only `SolidColor`
(`SkiaParagraph.native.kt:253-256`) — no brush/gradient text fill, `drawStyle` (`SkiaParagraph.native.kt:253-256`) - no brush/gradient text fill, `drawStyle`
(stroke), or non-`SrcOver` blendMode. Part of the reduced style coverage; extend on (stroke), or non-`SrcOver` blendMode. Part of the reduced style coverage; extend on
the local engine. the local engine.
- [ ] **P1** `SkiaParagraphEngine.kt:150-164` span segmentation drops partial `SpanStyle` - [ ] **P1** `SkiaParagraphEngine.kt:150-164` span segmentation drops partial `SpanStyle`
overlaps (keeps only fully-covering `start<=segStart && end>=segEnd`). Documented overlaps (keeps only fully-covering `start<=segStart && end>=segEnd`). Documented
divergence (`b63-upstream-text-mingw.md`) — widen to true interval segmentation. divergence (`b63-upstream-text-mingw.md`) - widen to true interval segmentation.
- [~] **P1** Accessibility absent — `ComposeOwner.kt:~340` builds a `semanticsOwner` never - [~] **P1** Accessibility absent - `ComposeOwner.kt:~340` builds a `semanticsOwner` never
traversed to any OS a11y API; `SemanticsRegion.native.kt intersect()/difference()` traversed to any OS a11y API; `SemanticsRegion.native.kt intersect()/difference()`
hardcode `false`. Not vendorable (NSAccessibility/UIA/AT-SPI). **Decision for 1.0.0: hardcode `false`. Not vendorable (NSAccessibility/UIA/AT-SPI). **Decision for 1.0.0:
out of scope for desktop.** The full a11y pipeline stays P2/out. out of scope for desktop.** The full a11y pipeline stays P2/out.
- [x] **P0** No-op `PlatformScreenReader` — `CompositionLocals.native.kt` - [x] **P0** No-op `PlatformScreenReader` - `CompositionLocals.native.kt`
`LocalPlatformScreenReader` default no longer throws (`error(...)`); it returns an `LocalPlatformScreenReader` default no longer throws (`error(...)`); it returns an
`InactivePlatformScreenReader` (`isActive = false`) so a11y-gated vendored code `InactivePlatformScreenReader` (`isActive = false`) so a11y-gated vendored code
degrades to "no reader present" instead of crashing. **DONE** — builds + runs clean. degrades to "no reader present" instead of crashing. **DONE** - builds + runs clean.
- [ ] **P2** `SkiaFonts.kt` fidelity: `resolveCache` unbounded (font-cache half of P3.4 - [ ] **P2** `SkiaFonts.kt` fidelity: `resolveCache` unbounded (font-cache half of P3.4
deferred); variation key passes `null` density (`:65,78`) so density-dependent `opsz` deferred); variation key passes `null` density (`:65,78`) so density-dependent `opsz`
in sp resolves wrong. `NamedFont.equals/hashCode` axis identity reportedly fixed — in sp resolves wrong. `NamedFont.equals/hashCode` axis identity reportedly fixed -
re-verify. re-verify.
### P2 — cosmetic / edge ### P2 - cosmetic / edge
- [ ] **P2** `BlendMode.Multiply` renders wrong — opaque cyan×yellow reads back blue; - [ ] **P2** `BlendMode.Multiply` renders wrong - opaque cyan×yellow reads back blue;
likely Metal premultiply in graphics-layer flatten, not the `toSkia()` map. Isolate on likely Metal premultiply in graphics-layer flatten, not the `toSkia()` map. Isolate on
the Skia draw path. the Skia draw path.
- [ ] **P2** Drag-OUT of window NOP — `ui/.../draganddrop/Sdl3DragAndDropOwner.kt:39` - [ ] **P2** Drag-OUT of window NOP - `ui/.../draganddrop/Sdl3DragAndDropOwner.kt:39`
`requestDragAndDropTransfer`; `DragAndDropSource.native.kt:36` no drag-shadow. SDL3 `requestDragAndDropTransfer`; `DragAndDropSource.native.kt:36` no drag-shadow. SDL3
has no portable start-drag → needs NSDraggingSession/DoDragDrop/XDND per-OS. has no portable start-drag → needs NSDraggingSession/DoDragDrop/XDND per-OS.
Drop-INTO works. Accepted 1.0 gap (SDL platform limit) — document. Drop-INTO works. Accepted 1.0 gap (SDL platform limit) - document.
- [ ] **P2** `NativeStringDelegate.native.kt:17` `toUpper/toLowerCase` ignore `locale` - [ ] **P2** `NativeStringDelegate.native.kt:17` `toUpper/toLowerCase` ignore `locale`
(Turkish i). Upstream `darwin` unvendorable (NSString). (Turkish i). Upstream `darwin` unvendorable (NSString).
- [ ] **P2** Prefetch scheduler NOP — `platform/PrefetchLocals.native.kt:18`, lazy lists - [ ] **P2** Prefetch scheduler NOP - `platform/PrefetchLocals.native.kt:18`, lazy lists
skip ahead-of-time composition (scroll-in jank). skip ahead-of-time composition (scroll-in jank).
- [ ] **P2** `ClipboardPasteState.hasClip` aliased to `hasText` - [ ] **P2** `ClipboardPasteState.hasClip` aliased to `hasText`
(`TextFieldSelectionState.native.kt:43`) — image-only clipboard undetected. (`TextFieldSelectionState.native.kt:43`) - image-only clipboard undetected.
- [ ] **P2** `ComposeOwner.kt` gaps: `autofill`/`autofillManager` null (`:324`), - [ ] **P2** `ComposeOwner.kt` gaps: `autofill`/`autofillManager` null (`:324`),
`hapticFeedBack` NOP (`:279`), deprecated `clipboardManager` NOP (`:287`, real `hapticFeedBack` NOP (`:279`), deprecated `clipboardManager` NOP (`:287`, real
`LocalClipboard` works), `textToolbar` stub (`:311`, touch-gated). No multi-monitor `LocalClipboard` works), `textToolbar` stub (`:311`, touch-gated). No multi-monitor
enumeration (`SDL_GetDisplays` unused). Most match upstream desktop — verify, then enumeration (`SDL_GetDisplays` unused). Most match upstream desktop - verify, then
leave or fill. leave or fill.
- [x] **P2** `installGlobals()` → `registerGenericFonts()`: VERIFIED not a hot-path cost — - [x] **P2** `installGlobals()` → `registerGenericFonts()`: VERIFIED not a hot-path cost -
`registerGenericFonts()` already early-returns on a `fRegistered` flag `registerGenericFonts()` already early-returns on a `fRegistered` flag
(`GenericFonts.kt:20-26`), so the per-event call is a single boolean check, not a (`GenericFonts.kt:20-26`), so the per-event call is a single boolean check, not a
re-register. Original premise inaccurate; no change needed. (Minor residue: the re-register. Original premise inaccurate; no change needed. (Minor residue: the
`ImeBridge.onSessionActiveChange` lambda is reassigned per event — negligible, left `ImeBridge.onSessionActiveChange` lambda is reassigned per event - negligible, left
as-is since it repoints per active window.) as-is since it repoints per active window.)
--- ---
@@ -521,14 +521,14 @@ re-vendoring.** The debt is **completing native-actual stubs**.
Static lib built from source per host (`scripts/build-sdl/build-all.py`, ref Static lib built from source per host (`scripts/build-sdl/build-all.py`, ref
`release-3.4.12`); linked into the exe via `sdl3.def` (`staticLibraries=libSDL3.a`). Today `release-3.4.12`); linked into the exe via `sdl3.def` (`staticLibraries=libSDL3.a`). Today
only tests/examples + Windows D3D12/GPU are off (`build-all.py:311-319`). `SDL_Init` uses only tests/examples + Windows D3D12/GPU are off (`build-all.py:311-319`). `SDL_Init` uses
**`SDL_INIT_VIDEO` only** (`SDL3Backend.kt:49`). **`SDL_INIT_VIDEO` only** (`Sdl3Backend.kt:49`).
**USED (keep ON):** Video/window, Events, Clipboard, Dialog (file open/save — no cheaper **USED (keep ON):** Video/window, Events, Clipboard, Dialog (file open/save - no cheaper
substitute; Linux uses portal/zenity, keep deps), OpenGL + Metal contexts, Render (2D CPU- substitute; Linux uses portal/zenity, keep deps), OpenGL + Metal contexts, Render (2D CPU-
raster blit path: `SkiaSurfaceBridge` uploads to `SDL_Texture` + `SDL_RenderPresent`), raster blit path: `SkiaSurfaceBridge` uploads to `SDL_Texture` + `SDL_RenderPresent`),
Filesystem, Cursor, Locale, System theme, Text input/IME, OpenURL, timing/hints. Filesystem, Cursor, Locale, System theme, Text input/IME, OpenURL, timing/hints.
**UNUSED (disable — zero references, no transitive need):** Audio, Joystick (→disables **UNUSED (disable - zero references, no transitive need):** Audio, Joystick (→disables
Gamepad), Haptic, HIDAPI, Sensor, Power, Camera, GPU API, Offscreen video driver, virtual Gamepad), Haptic, HIDAPI, Sensor, Power, Camera, GPU API, Offscreen video driver, virtual
joystick. joystick.
@@ -537,7 +537,7 @@ joystick.
`-DSDL_POWER=OFF`, `-DSDL_CAMERA=OFF`, `-DSDL_GPU=OFF`, `-DSDL_OFFSCREEN=OFF`, `-DSDL_POWER=OFF`, `-DSDL_CAMERA=OFF`, `-DSDL_GPU=OFF`, `-DSDL_OFFSCREEN=OFF`,
`-DSDL_VIRTUAL_JOYSTICK=OFF`. Promoted `-DSDL_GPU=OFF` from the Windows-only branch to `-DSDL_VIRTUAL_JOYSTICK=OFF`. Promoted `-DSDL_GPU=OFF` from the Windows-only branch to
the shared list; kept the per-host `-DSDL_RENDER_D3D12=OFF` (Windows). (Skipped the shared list; kept the per-host `-DSDL_RENDER_D3D12=OFF` (Windows). (Skipped
`-DSDL_DISABLE_INSTALL_DOCS` — not a real SDL3 option; the build's `cmake --install` `-DSDL_DISABLE_INSTALL_DOCS` - not a real SDL3 option; the build's `cmake --install`
is relied upon.) All ten confirmed zero-reference by grep before disabling. is relied upon.) All ten confirmed zero-reference by grep before disabling.
- [~] **P1** After the flag change, rebuild + run apps to confirm no regression. - [~] **P1** After the flag change, rebuild + run apps to confirm no regression.
**DONE on macOS/Metal:** `build-all.py` rebuilt libSDL3.a clean with the new flags; **DONE on macOS/Metal:** `build-all.py` rebuilt libSDL3.a clean with the new flags;
@@ -548,17 +548,17 @@ joystick.
- [x] **P2** Prune now-unreferenced macOS frameworks from `sdl3.def`. **DONE:** verified - [x] **P2** Prune now-unreferenced macOS frameworks from `sdl3.def`. **DONE:** verified
against the regenerated `libs/SDL3/lib/pkgconfig/sdl3.pc` `Libs:` line (source of against the regenerated `libs/SDL3/lib/pkgconfig/sdl3.pc` `Libs:` line (source of
truth), which after slimming no longer references `CoreAudio`, `AudioToolbox`, truth), which after slimming no longer references `CoreAudio`, `AudioToolbox`,
`AVFoundation`, `GameController`, `ForceFeedback`, or weak `CoreHaptics` — removed all `AVFoundation`, `GameController`, `ForceFeedback`, or weak `CoreHaptics` - removed all
six from `linkerOpts.osx` (kept `CoreMedia`, which the `.pc` still lists). Demo relinks six from `linkerOpts.osx` (kept `CoreMedia`, which the `.pc` still lists). Demo relinks
clean. (Left Linux `linkerOpts` alone — those `-l` entries serve Skia/GL/X11, not the clean. (Left Linux `linkerOpts` alone - those `-l` entries serve Skia/GL/X11, not the
disabled SDL subsystems.) disabled SDL subsystems.)
- [ ] **P2** (higher-risk, flag-don't-apply) Render-driver pruning to software-only. Only - [ ] **P2** (higher-risk, flag-don't-apply) Render-driver pruning to software-only. Only
the CPU-raster fallback uses `SDL_Render`; GL/Metal go direct. But the CPU-raster fallback uses `SDL_Render`; GL/Metal go direct. But
`SDL_CreateRenderer(window, null)` (`SDL3Backend.kt:117`) lets SDL pick the first `SDL_CreateRenderer(window, null)` (`Sdl3Backend.kt:117`) lets SDL pick the first
driver — with only software present it picks software (fine) but couples the fallback driver - with only software present it picks software (fine) but couples the fallback
to that assumption. **Conservative: leave render drivers alone.** to that assumption. **Conservative: leave render drivers alone.**
**Do NOT** add `-DSDL_DISABLE_INSTALL` — the build relies on `cmake --install` **Do NOT** add `-DSDL_DISABLE_INSTALL` - the build relies on `cmake --install`
(`build-all.py:342`) to stage `libSDL3.a` + headers. `SDL_DYNAMIC_API` auto-disables for (`build-all.py:342`) to stage `libSDL3.a` + headers. `SDL_DYNAMIC_API` auto-disables for
static builds (no flag needed). **Risk:** a future consumer app needing SDL audio/gamepad static builds (no flag needed). **Risk:** a future consumer app needing SDL audio/gamepad
requires a static-lib rebuild (build-time, not a code change). requires a static-lib rebuild (build-time, not a code change).
@@ -569,16 +569,16 @@ requires a static-lib rebuild (build-time, not a code change).
Prior PLAN Phases 0–3 landed; §12 measurement showed **no steady-state perf gap on Prior PLAN Phases 0–3 landed; §12 measurement showed **no steady-state perf gap on
macOS/Metal**. Remaining perf question (if any) is Windows-GL or heavy-interaction macOS/Metal**. Remaining perf question (if any) is Windows-GL or heavy-interaction
specific. Retained-layer engine is byte-for-byte upstream — not the gap. specific. Retained-layer engine is byte-for-byte upstream - not the gap.
- [ ] **P2** WON'T-FIX confirmed, keep as-is (don't relitigate): P0.4 per-frame full-surface - [ ] **P2** WON'T-FIX confirmed, keep as-is (don't relitigate): P0.4 per-frame full-surface
clear + P0.6 per-frame Metal drawable reacquire (`CAMetalDrawable` single-use per clear + P0.6 per-frame Metal drawable reacquire (`CAMetalDrawable` single-use per
frame, matches upstream `MetalRedrawer`). frame, matches upstream `MetalRedrawer`).
- [ ] **P2** P2.2 upstream `GlobalSnapshotManager` invalidation-driven scheduling — - [ ] **P2** P2.2 upstream `GlobalSnapshotManager` invalidation-driven scheduling -
deferred; coalescing half done by P1.3; full manager is skiko-windowing-coupled deferred; coalescing half done by P1.3; full manager is skiko-windowing-coupled
(an explicit renderer non-goal — skiko-windowing-coupled). Leave deferred unless a (an explicit renderer non-goal - skiko-windowing-coupled). Leave deferred unless a
Windows-GL perf gap surfaces. Windows-GL perf gap surfaces.
- [ ] **P2** `SkiaImageCache` font-cache half of P3.4 — evicted typefaces stay referenced by - [ ] **P2** `SkiaImageCache` font-cache half of P3.4 - evicted typefaces stay referenced by
live paragraphs + `TypefaceFontProvider` (no clean unregister) so eviction frees no live paragraphs + `TypefaceFontProvider` (no clean unregister) so eviction frees no
memory. Native-resource lifecycle backstop is the periodic `GC.collect()` nudge (top- memory. Native-resource lifecycle backstop is the periodic `GC.collect()` nudge (top-
level `Paragraph` a live `Text` holds has no Compose dispose seam). Accept for 1.0 or level `Paragraph` a live `Text` holds has no Compose dispose seam). Accept for 1.0 or
@@ -587,9 +587,9 @@ specific. Retained-layer engine is byte-for-byte upstream — not the gap.
shadows via `SkiaGraphicsContext.setLightingInfo` + relocate shadows via `SkiaGraphicsContext.setLightingInfo` + relocate
`prepareTransformationMatrix`; deltas D2–D6). Buys vendoring cleanliness at `prepareTransformationMatrix`; deltas D2–D6). Buys vendoring cleanliness at
shadow-lighting/hit-test regression risk (current `NativeShadowCanvas` shadows render, shadow-lighting/hit-test regression risk (current `NativeShadowCanvas` shadows render,
draw=0.08ms). Low ROI — deferred, needs full verify-mac + parity. draw=0.08ms). Low ROI - deferred, needs full verify-mac + parity.
- [ ] **P2** Profile the shipped **Windows-GL** binary (`CDN_PROFILE=1`) under heavy - [ ] **P2** Profile the shipped **Windows-GL** binary (`CDN_PROFILE=1`) under heavy
interaction before ship (present phase is vsync-capped by display refresh — profile on interaction before ship (present phase is vsync-capped by display refresh - profile on
the target monitor). Only open perf unknown. the target monitor). Only open perf unknown.
--- ---
@@ -602,50 +602,50 @@ and the `ui-text.platform` font layer (= P3.2 font resolver). The 1076 "extra" d
umbrella-repo modules the tool can't compare + version skew, not invented surface. umbrella-repo modules the tool can't compare + version skew, not invented surface.
- [ ] **P0** Re-pin refs to Compose **1.12.0 stable** when it ships (currently - [ ] **P0** Re-pin refs to Compose **1.12.0 stable** when it ships (currently
`v1.12.0-beta03+dev4483` — no clean beta03 tag on Maven). Bump both `v1.12.0-beta03+dev4483` - no clean beta03 tag on Maven). Bump both
`scripts/compose-fork/compose.properties` refs, re-sync (`scripts/compose-fork/sync.sh`), `scripts/compose-fork/compose.properties` refs, re-sync (`scripts/compose-fork/sync.sh`),
let the build surface breakage. let the build surface breakage.
- [ ] **P0** Bump `vComposeJvmVersion` in `:demo` / `:apidemo` / `:material-symbols` (JVM - [ ] **P0** Bump `vComposeJvmVersion` in `:demo` / `:apidemo` / `:material-symbols` (JVM
parity leg currently forced to beta02 by documented skew — native leads). parity leg currently forced to beta02 by documented skew - native leads).
- [ ] **P1** Run `check-vendor-drift.py` after the ref bump — re-stamp `VENDOR-BASE` on the - [ ] **P1** Run `check-vendor-drift.py` after the ref bump - re-stamp `VENDOR-BASE` on the
10 manual vendors, hand-reconcile any that actually changed base..pin. 10 manual vendors, hand-reconcile any that actually changed base..pin.
- [~] **P0** WIN-SMOKE fidelity pass (Windows host only, pre-ship gate): the Mac runbook - [~] **P0** WIN-SMOKE fidelity pass (Windows host only, pre-ship gate): the Mac runbook
cannot cover the shipped mingwX64 binary. Assert: (1) NotoSans `FontMetrics` dump cannot cover the shipped mingwX64 binary. Assert: (1) NotoSans `FontMetrics` dump
native-vs-JVM-Windows (§1b acceptance) — **✅ DONE 2026-08-04 (`--metricsprobe`==`--metrics`)**; native-vs-JVM-Windows (§1b acceptance) - **✅ DONE 2026-08-04 (`--metricsprobe`==`--metrics`)**;
(2) `\t`/control-char render clean (§1a) — **✅ DONE**; (3) the Windows-only `PrintWindow` (2) `\t`/control-char render clean (§1a) - **✅ DONE**; (3) the Windows-only `PrintWindow`
probe — pending (interactive); (4) the common-metadata publish job — compile gate now probe - pending (interactive); (4) the common-metadata publish job - compile gate now
**✅ GREEN (2026-08-04, see the fix below)**; the actual publish + downstream-consume smoke **✅ GREEN (2026-08-04, see the fix below)**; the actual publish + downstream-consume smoke
still owed. Remaining: (3) `PrintWindow` probe + the interactive caret-height check for the still owed. Remaining: (3) `PrintWindow` probe + the interactive caret-height check for the
§1b LineMetrics fix. §1b LineMetrics fix.
- [ ] **P0** apiDump is **host-specific** — do NOT commit macOS dumps. Only the **Windows - [ ] **P0** apiDump is **host-specific** - do NOT commit macOS dumps. Only the **Windows
publish job compiles common metadata** (owns the root KotlinMultiplatform publications publish job compiles common metadata** (owns the root KotlinMultiplatform publications
— the only host declaring every target, so only its `.module` files carry the full - the only host declaring every target, so only its `.module` files carry the full
variant table; macOS-published roots left v0.1.15 without mingwX64 variants). Test variant table; macOS-published roots left v0.1.15 without mingwX64 variants). Test
`gradlew :<module>:compileCommonMainKotlinMetadata` before tagging; publish from `gradlew :<module>:compileCommonMainKotlinMetadata` before tagging; publish from
Windows. Windows.
- [x] **P0** **FIXED (2026-08-04) — common-metadata compile is now GREEN** (`compileCommonMainKotlin - [x] **P0** **FIXED (2026-08-04) - common-metadata compile is now GREEN** (`compileCommonMainKotlin
Metadata` across all modules → BUILD SUCCESSFUL). Was RED, blocking the Windows publish. Metadata` across all modules → BUILD SUCCESSFUL). Was RED, blocking the Windows publish.
Running `:material3:compileCommonMainKotlinMetadata` (or the aggregate) had failed at Running `:material3:compileCommonMainKotlinMetadata` (or the aggregate) had failed at
`:foundation:compileNativeMainKotlinMetadata` (pulled in via `:foundation:allMetadataJar`): `:foundation:compileNativeMainKotlinMetadata` (pulled in via `:foundation:allMetadataJar`):
`Scrollbar.skiko.kt` + `v2/Scrollbar.skiko.kt` — `Declaration annotated with `Scrollbar.skiko.kt` + `v2/Scrollbar.skiko.kt` - `Declaration annotated with
'@OptionalExpectation' can only be used in common module sources` '@OptionalExpectation' can only be used in common module sources`
(`OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE`). **Root cause:** upstream keeps these (`OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE`). **Root cause:** upstream keeps these
`.skiko.kt` files in `skikoMain`, which INCLUDES the JVM-desktop target, so `@JvmName`/ `.skiko.kt` files in `skikoMain`, which INCLUDES the JVM-desktop target, so `@JvmName`/
`@JvmStatic`/`@file:JvmName` resolve; the port vendors them into **native-only `nativeMain`** `@JvmStatic`/`@file:JvmName` resolve; the port vendors them into **native-only `nativeMain`**
(no JVM target under it), so those JVM annotations are orphaned and the *metadata* compile (no JVM target under it), so those JVM annotations are orphaned and the *metadata* compile
rejects them (per-target compile tolerates them — that's why the APPS build fine, but the rejects them (per-target compile tolerates them - that's why the APPS build fine, but the
publish's `allMetadataJar` recompiles nativeMain metadata and dies). `:ui` and below pass; publish's `allMetadataJar` recompiles nativeMain metadata and dies). `:ui` and below pass;
`:foundation` is the first break. **Blast radius:** 14 vendored native files use `@Jvm*` `:foundation` is the first break. **Blast radius:** 14 vendored native files use `@Jvm*`
(`grep -rE "@file:JvmName|@JvmName|@JvmStatic" .../src/vendor/native .../src/nativeMain`); 11 (`grep -rE "@file:JvmName|@JvmName|@JvmStatic" .../src/vendor/native .../src/nativeMain`); 11
are in the GITIGNORED, sync-regenerated `src/vendor/native/` tree, so per-file edits DON'T are in the GITIGNORED, sync-regenerated `src/vendor/native/` tree, so per-file edits DON'T
survive a re-sync. **Fix (build/sync-level, not per-file):** either (a) a `sync.sh` survive a re-sync. **Fix (build/sync-level, not per-file):** either (a) a `sync.sh`
post-step that injects `@file:Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE")` post-step that injects `@file:Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE")`
(or strips `@Jvm*`, which are no-ops with no JVM library target) into vendored native files — (or strips `@Jvm*`, which are no-ops with no JVM library target) into vendored native files -
there's precedent: `components-resources/…/ResourceState.blocking.kt` already carries that there's precedent: `components-resources/…/ResourceState.blocking.kt` already carries that
suppress; or (b) a KGP-level relaxation of the native metadata compilation. Needs a real suppress; or (b) a KGP-level relaxation of the native metadata compilation. Needs a real
publish + downstream-consume verification after. Dedicated effort — likely THE reason the publish + downstream-consume verification after. Dedicated effort - likely THE reason the
Windows metadata publish never went green. Windows metadata publish never went green.
**FIX APPLIED (2026-08-04):** the root turned out narrower — sync.py ALREADY injects the K2 **FIX APPLIED (2026-08-04):** the root turned out narrower - sync.py ALREADY injects the K2
`@file:Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE", "LESS_VISIBLE_TYPE_ACCESS_IN_INLINE")` `@file:Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE", "LESS_VISIBLE_TYPE_ACCESS_IN_INLINE")`
into every `src/vendor/` .kt (`sync.py:313`), so the regenerated tree was fine. The only gap into every `src/vendor/` .kt (`sync.py:313`), so the regenerated tree was fine. The only gap
was the two COMMITTED manual vendors in `src/nativeMain` that sync doesn't regenerate: was the two COMMITTED manual vendors in `src/nativeMain` that sync doesn't regenerate:
@@ -657,14 +657,14 @@ umbrella-repo modules the tool can't compare + version skew, not invented surfac
- [ ] **P1** Version bump to `1.0.0` across published coords once the above are green. - [ ] **P1** Version bump to `1.0.0` across published coords once the above are green.
- [x] **P0** Doc-hygiene blocker: `CLAUDE.md` documentation map referenced `RENDERER.md`, - [x] **P0** Doc-hygiene blocker: `CLAUDE.md` documentation map referenced `RENDERER.md`,
`SKIKO-MINGW-FEASIBILITY.md`, and `TODO.md`. **DONE:** removed `RENDERER.md` + `SKIKO-MINGW-FEASIBILITY.md`, and `TODO.md`. **DONE:** removed `RENDERER.md` +
`SKIKO-MINGW-FEASIBILITY.md` (stale/historical — current renderer essentials inlined in `SKIKO-MINGW-FEASIBILITY.md` (stale/historical - current renderer essentials inlined in
CLAUDE.md's doc map, the rest recoverable from git history); fixed the `CLAUDE.md` CLAUDE.md's doc map, the rest recoverable from git history); fixed the `CLAUDE.md`
line-1 `ean` typo; dropped the `TODO.md` link (never committed) → its content is PLAN.md line-1 `ean` typo; dropped the `TODO.md` link (never committed) → its content is PLAN.md
§2. `git grep` confirms no remaining links to any deleted doc. §2. `git grep` confirms no remaining links to any deleted doc.
--- ---
## 6. Vendor the upstream skiko text engine (next major refactor — approved 2026-08-04) ## 6. Vendor the upstream skiko text engine (next major refactor - approved 2026-08-04)
**Goal:** honour "VENDOR ALL WE CAN". Delete the hand-rolled reduced text engine **Goal:** honour "VENDOR ALL WE CAN". Delete the hand-rolled reduced text engine
(`SkiaParagraph.native.kt` + `NativeParagraphOps` seam + `SkiaParagraphEngine.kt` + (`SkiaParagraph.native.kt` + `NativeParagraphOps` seam + `SkiaParagraphEngine.kt` +
@@ -674,21 +674,21 @@ umbrella-repo modules the tool can't compare + version skew, not invented surfac
(`vendor/common/…/Paragraph.kt:56`) whose `actual` sits in the **skiko-FREE `nativeMain`**, so a (`vendor/common/…/Paragraph.kt:56`) whose `actual` sits in the **skiko-FREE `nativeMain`**, so a
sealed implementer can't touch skiko → the hand seam. `nativeMain` is skiko-free because it's the sealed implementer can't touch skiko → the hand seam. `nativeMain` is skiko-free because it's the
shared parent of the **official-skiko** (mac/linux, `libs.skiko`) and **fork-skiko** (mingw, shared parent of the **official-skiko** (mac/linux, `libs.skiko`) and **fork-skiko** (mingw,
`com.bitsycore.skiko:skiko`) legs — two different artifacts, so the shared parent can carry neither. `com.bitsycore.skiko:skiko`) legs - two different artifacts, so the shared parent can carry neither.
**Why it's feasible NOW (all-skiko + srcDir reuse):** every native leg already reaches **Why it's feasible NOW (all-skiko + srcDir reuse):** every native leg already reaches
`src/skikoRendererMain/kotlin` (mingw via `kotlin.srcDir(...)` in `skikoRendererMingwSharedMain`, `src/skikoRendererMain/kotlin` (mingw via `kotlin.srcDir(...)` in `skikoRendererMingwSharedMain`,
`ui-text/build.gradle.kts:54`), and the fork exposes the SAME `org.jetbrains.skia.*` Kotlin API. So `ui-text/build.gradle.kts:54`), and the fork exposes the SAME `org.jetbrains.skia.*` Kotlin API. So
the `Paragraph` **actual + engine can move DOWN into the skiko source set** (served to all targets) the `Paragraph` **actual + engine can move DOWN into the skiko source set** (served to all targets)
instead of skiko-free `nativeMain` — which lets upstream's skiko files compile as-is. instead of skiko-free `nativeMain` - which lets upstream's skiko files compile as-is.
**⚠️ SPIKE FINDING (2026-08-04) — the real gate is the FONT LOADER, not the source-set move.** **⚠️ SPIKE FINDING (2026-08-04) - the real gate is the FONT LOADER, not the source-set move.**
A read-only feasibility map (no code changed) found two layers: A read-only feasibility map (no code changed) found two layers:
- **Layer 1 — source-set / sealed move: FEASIBLE.** The `actual sealed interface Paragraph` - **Layer 1 - source-set / sealed move: FEASIBLE.** The `actual sealed interface Paragraph`
(`vendor/native/Paragraph.native.kt:36`, signature-only mirror) + its implementer + the 11 factory (`vendor/native/Paragraph.native.kt:36`, signature-only mirror) + its implementer + the 11 factory
actuals can move to `skikoRendererMain` (all leaves reach it; sealed same-source-set holds). Just actuals can move to `skikoRendererMain` (all leaves reach it; sealed same-source-set holds). Just
mechanical. mechanical.
- **Layer 2 — font model: THIS is the blocker, and it's the SAME task as §2-P1 `PlatformFontLoader`.** - **Layer 2 - font model: THIS is the blocker, and it's the SAME task as §2-P1 `PlatformFontLoader`.**
Upstream's engine (`SkiaParagraphIntrinsics.skiko.kt:38,61`) resolves typefaces through Upstream's engine (`SkiaParagraphIntrinsics.skiko.kt:38,61`) resolves typefaces through
`FontFamily.Resolver` → `PlatformFontLoader`. The port's `SdlPlatformFontLoader` `FontFamily.Resolver` → `PlatformFontLoader`. The port's `SdlPlatformFontLoader`
(`font/FontFamilyResolver.native.kt:30-49`) is a **NOP** (`loadBlocking`/`awaitLoad`→`Unit`, (`font/FontFamilyResolver.native.kt:30-49`) is a **NOP** (`loadBlocking`/`awaitLoad`→`Unit`,
@@ -699,15 +699,15 @@ A read-only feasibility map (no code changed) found two layers:
- **Consequence:** the verbatim engine-vendor REQUIRES first implementing upstream's font-loader path - **Consequence:** the verbatim engine-vendor REQUIRES first implementing upstream's font-loader path
(vendor `SkiaFontLoader.skiko.kt` as the reference) so the port's `SkiaFonts` icon/variable-axis (vendor `SkiaFontLoader.skiko.kt` as the reference) so the port's `SkiaFonts` icon/variable-axis
model rides upstream's `PlatformFontLoader`/`FontLoadResult` seam. This is the deferred high-risk P1 model rides upstream's `PlatformFontLoader`/`FontLoadResult` seam. This is the deferred high-risk P1
in §2 — it and this §6 are one effort. **Intermediate option:** vendor the engine files but keep in §2 - it and this §6 are one effort. **Intermediate option:** vendor the engine files but keep
`SkiaFonts` via a Rule-3 edit at the one resolve call site (vendors most files verbatim, 1–2 stay `SkiaFonts` via a Rule-3 edit at the one resolve call site (vendors most files verbatim, 1–2 stay
hand-reconciled). **Verification needs macOS + Linux hosts** (parity), so this is a dedicated hand-reconciled). **Verification needs macOS + Linux hosts** (parity), so this is a dedicated
effort, not a Windows-only session — do it with mac access, on its own branch. effort, not a Windows-only session - do it with mac access, on its own branch.
**Plan (spike on a branch, verify on THIS Windows host — build + render mingwX64):** **Plan (spike on a branch, verify on THIS Windows host - build + render mingwX64):**
1. Move the `Paragraph`/`ParagraphIntrinsics` **actuals** into `skikoRendererMain` (reused by mingw 1. Move the `Paragraph`/`ParagraphIntrinsics` **actuals** into `skikoRendererMain` (reused by mingw
via the existing `srcDir`). Confirm the `expect sealed` actual is accepted there for every leaf via the existing `srcDir`). Confirm the `expect sealed` actual is accepted there for every leaf
target (the b63 memo hit a sealed-in-`nativeMain` wall — test whether hosting the actual in the target (the b63 memo hit a sealed-in-`nativeMain` wall - test whether hosting the actual in the
skiko set clears it; the sealed same-module rule is per-target-compilation, which should hold). skiko set clears it; the sealed same-module rule is per-target-compilation, which should hold).
2. Vendor VERBATIM into `src/vendor/skikoRenderer/`: `SkiaParagraph.skiko.kt`, 2. Vendor VERBATIM into `src/vendor/skikoRenderer/`: `SkiaParagraph.skiko.kt`,
`ParagraphBuilder.skiko.kt`, `SkiaParagraphIntrinsics.skiko.kt`, `SkiaTextPaint.skiko.kt`, `ParagraphBuilder.skiko.kt`, `SkiaParagraphIntrinsics.skiko.kt`, `SkiaTextPaint.skiko.kt`,
@@ -715,7 +715,7 @@ A read-only feasibility map (no code changed) found two layers:
`TextStyle.skiko.kt`; add them to `ui-text/compose-fork.txt`; delete the hand-rolled files. `TextStyle.skiko.kt`; add them to `ui-text/compose-fork.txt`; delete the hand-rolled files.
3. Reconcile the ~7 hierarchy-crossing `expect/actual` pairs the inverted layout needed; keep the 3. Reconcile the ~7 hierarchy-crossing `expect/actual` pairs the inverted layout needed; keep the
`SkiaFonts` family/variable-axis model (it's the port's real value-add) behind upstream's seams. `SkiaFonts` family/variable-axis model (it's the port's real value-add) behind upstream's seams.
4. **Fork-only Rule-3 edit:** upstream's code calls `paragraph.lineMetrics` — the fork's extern-C 4. **Fork-only Rule-3 edit:** upstream's code calls `paragraph.lineMetrics` - the fork's extern-C
binding mis-decodes ascent/descent (§1b finding). Either carry the guarded reconstruction (from binding mis-decodes ascent/descent (§1b finding). Either carry the guarded reconstruction (from
`SkiaParagraphOps.lineMetrics()`) into the vendored `SkiaParagraph.skiko.kt` as a Rule-3 edit, or `SkiaParagraphOps.lineMetrics()`) into the vendored `SkiaParagraph.skiko.kt` as a Rule-3 edit, or
fix it in the fork DLL. **Bonus:** vendoring `ParagraphBuilder.skiko.kt` verbatim wires fix it in the fork DLL. **Bonus:** vendoring `ParagraphBuilder.skiko.kt` verbatim wires
@@ -727,10 +727,240 @@ A read-only feasibility map (no code changed) found two layers:
proves infeasible, fall back to keeping the seam but at least converting the hand files to tracked proves infeasible, fall back to keeping the seam but at least converting the hand files to tracked
Rule-3 vendors (like `FontRasterizationSettings.native.kt` now has a `VENDOR-BASE` line). Rule-3 vendors (like `FontRasterizationSettings.native.kt` now has a `VENDOR-BASE` line).
### 7c. `demo --multiwintest` segfault on mingwX64 - ✅ FIXED
The multi-window probe exited 139 with no output, so it had silently been
asserting nothing. Root cause: an OpenGL context is per-WINDOW but "current" is
per-THREAD, and `SDL_GL_MakeCurrent` ran only once at creation
(`Sdl3Backend.init`) with nothing re-binding it per frame. With two windows open,
whichever initialised last owned the thread - so window A issued its draw and
swap against window B's context (silent corruption while both lived), and
destroying B left the current context dangling, faulting in `present()`.
Fixed in `SkiaGLBridge` by binding the window's own context at every entry point
that touches GL: `init`, `ensureSize` (before its unchanged-size early return -
this is the per-frame bind that covers the draw phase), `present`, and `destroy`
(so a window frees its GPU objects against its own context). Purely additive, 29
lines.
Single-window rendering is bit-identical before and after (same mean RGB
[46.6 44.4 51.5] and 167 distinct colours on the Buttons screenshot), which is
expected: SDL short-circuits a redundant bind. `--multiwintest` now PASSes.
NOTE the multi-window path was never correct, not just at teardown - two live
windows were sharing one context. Anything that looked like flicker or
cross-window corruption before this is explained by it.
## Accepted 1.0.0 gaps (documented, not fixed) ## Accepted 1.0.0 gaps (documented, not fixed)
Drag-OUT of window (SDL platform limit; drop-IN works), full accessibility pipeline (out of Drag-OUT of window (SDL platform limit; drop-IN works), full accessibility pipeline (out of
scope for desktop 1.0 — but ship the non-throwing `PlatformScreenReader` no-op), the scope for desktop 1.0 - but ship the non-throwing `PlatformScreenReader` no-op), the
hand-rolled text engine as an architectural deviation (§2 P1), per-focus hand-rolled text engine as an architectural deviation (§2 P1), per-focus
`SDL_StartTextInput/StopTextInput`, `loadImageBitmap`/`loadSvgPainter` (JVM `InputStream` `SDL_StartTextInput/StopTextInput`, `loadImageBitmap`/`loadSvgPainter` (JVM `InputStream`
signatures, N/A on K/N). signatures, N/A on K/N).
## 8. material3-adaptive ported to desktop native (2026-09-22) - ✅ LANDED
Asked for in [issue #4](https://github.com/bitsycore/compose-desktop-native/issues/4):
`org.jetbrains.compose.material3.adaptive:*` 1.3.0-rc01 publishes `ios_arm64
ios_simulator_arm64 macos_arm64` and nothing else - no linux, no mingw. (It DOES
carry macosArm64, which answers the reporter's open question.)
**The port needed zero source edits.** Two things made it a pure re-target:
1. Every platform actual the native legs want already exists upstream -
`adaptive/nonAndroidMain` (`calculatePosture`), `adaptive-layout/skikoMain`
(the ~75 l10n tables + `Strings` + the drag handle) and
`adaptive-layout/nativeMain` (`identityHashCode`, `isGraphicsLayerElement`).
2. `androidx.window:window-core` - the dependency `adaptive` api-exposes, and the
obvious suspect for why the artifacts stop at apple - has published
`linux_x64 linux_arm64 mingw_x64 macos_arm64` klibs all along, under BOTH the
google and jetbrains coordinates.
So the artifacts were missing purely because upstream never enabled the targets.
**Landed** as four modules under `compose/material3/adaptive/`
(`com.bitsycore.compose.material3.adaptive:*`), 125 files vendored verbatim off the
existing `COMPOSE_CORE_REF` pin - no new SET_REPO. Upstream's `skikoMain` lands in
`src/vendor/native/` beside upstream's own `nativeMain`; they actualise disjoint
expects so they share one source set. Both bridges substitute all four. `demo --screen=Adaptive` renders a
`ListDetailPaneScaffold`, and `demo --adaptivetest` drives one window
wide -> narrow -> wide and asserts the scaffold folds and unfolds, which is what
actually proves the resize chain end to end (SDL resize -> the owner's
snapshot-backed containerSize -> recomposition -> BoxWithConstraints re-measure ->
a new PaneScaffoldDirective -> a new scaffold value).
The demo screen computes its directive from the width IT gets rather than taking
the `rememberListDetailPaneScaffoldNavigator` default, which is the window size
class: the shell spends 190dp on the sidebar plus 48dp of padding, so defaulting
folds ~240dp too late and leaves two panes in a region already too cramped for
them. Pane-hosting apps that are not full-window should do the same.
One wrinkle worth remembering, and a textbook case of the granular-metadata trap
already documented in CLAUDE.md: because `adaptive` is SUBSTITUTED on native
configs, KGP drops its transitives from the commonMain metadata classpath, so the
shared demo screen could not see `WindowSizeClass` until `window-core` was declared
DIRECTLY in `demo`'s commonMain.
Not yet exercised: `adaptive-navigation3` builds and dumps API but has no demo
screen - the nav3 SceneStrategy path is unproven at runtime.
## 7. Post-1.12.0 open items (opened 2026-09-12)
### 7a. JVM parity reference used a DIFFERENT default font than native - ✅ FIXED
Symptom (apidemo, `SourceTag` in `InheritanceUi.kt`): the "Session" / "Pack" pill is
`Box(padding(horizontal = 5.dp, vertical = 1.dp)) { Text(fontSize = 9.sp) }` - no explicit
vertical centering, so the pill's look is entirely decided by where the glyphs sit inside the
Text node's measured paragraph box. On the JVM leg the label looks vertically centred in the
pill; on native it does not.
**Not a centering bug - a font-resolution divergence.** The two stacks render that label in
two different typefaces, which have different ascent / descent / line-gap, so the glyph run
sits at a different height inside an equally-tall box:
- NATIVE: `FontFamily.Default` resolves to the BUNDLED **NotoSans** -
`SkiaFonts.defaultTypeface` (`compose/ui/ui-text/src/skikoRendererMain/.../SkiaFonts.kt`)
loads `font/NotoSans.ttf` from data.kres and registers it as "the fallback for every
unresolved family".
- JVM: nothing registers NotoSans as the default. `apidemo/src/jvmMain/kotlin/Fonts.jvm.kt`
only defines `monoFontFamily`; `jvmProcessResources` stages the .ttf on the classpath but
never installs it. So `FontFamily.Default` is Skiko's SYSTEM default (Segoe UI on Windows).
This is a defect in the PARITY HARNESS, not (necessarily) in the renderer: every `Text` that
doesn't name a `fontFamily` is being compared against a different typeface, so any such
difference is uninterpretable. It likely masks or manufactures other "port bugs".
**Fixed** by giving apidemo a `defaultFontFamily` expect/actual (`Fonts.kt`) that loads the
bundled `font/NotoSans.ttf` on BOTH legs - native through `IconFont.register` +
`namedFontFamily`, jvm from the classpath copy `jvmProcessResources` already stages - and
stamping it across the M3 `Typography` (`withDefaultFontFamily`), since typography styles
carry their own `fontFamily` and overriding `LocalTextStyle` alone would not reach them.
This is the SAME treatment `:demo`'s parity leg already had (see the `notoSans` /
`notoTypography` block in `demo/src/jvmMain/kotlin/MainJvm.kt`, commented "so the parity %
measures real divergence, not 'different default typeface'"). apidemo had simply never been
given it. Verified: the SourceTag pill is now vertically centred on both stacks.
### 7a-bis. apidemo drift check is now mechanised - and two residuals found
`:apidemo` had no way to capture itself, so drift could only be eyeballed. Both legs now take
`--screenshot=<path>`:
apidemo.exe --screenshot=out.bmp # native: GPU readback
./gradlew :apidemo:run --args=--screenshot=out.png # jvm: ImageComposeScene
Both render OFFSCREEN to quiescence (virtual 60fps clock, infinite animations frozen via
upstream's `InfiniteAnimationPolicy`, capture after 3 quiet frames), mirroring `:demo`'s
parity path. Neither needs a visible window, so the capture is immune to occlusion - a
screen-scrape approach was tried first and grabbed whatever happened to be in front.
First run after the font fix, 1240x820: **2.9% of pixels differ at all, 2.1% by >32/255.**
Two residuals, neither a text-metrics bug:
- **Inherited lineHeight leading distribution - ✅ FIXED.** The port never forwarded
`topRatio` to skiko. Upstream's `ParagraphBuilder.skiko.kt` sets `res.topRatio = topRatio`
on EVERY SkTextStyle, sourced from `LineHeightStyle.Alignment.topRatio`; the port set
`ts.height` and `heightMode` but left `topRatio` alone, so skiko fell back to its own
default (-1 = split the extra leading proportionally to the font's ascent:descent). For a
line box much taller than the glyphs that pushes them visibly DOWN - while Material 3's
type styles ask for `Alignment.Center` (topRatio 0.5).
One line in `SkiaParagraphEngine.kt`'s SkTextStyle builder, with `Alignment.Proportional`
as the fallback exactly like upstream, so text specifying no lineHeightStyle is unchanged.
`topRatio` is present on both the official skiko and the bitsycore mingw fork.
Measured on apidemo (1240x820), whole window, native vs jvm:
| | differ | >32/255 |
|--------------------|--------|---------|
| before | 2.94% | 2.14% |
| after | 1.52% | 0.29% |
The Session/Pack pills are now pixel-identical. This ALSO killed the "constant ~2px
vertical offset" logged alongside it - that was the same bug compounding through stacked
text rows, not an independent issue: shifting the jvm capture now makes the match WORSE
(0.29% at +0px vs 2.13% at +1px), where before +2px halved the error. The residual 0.29%
is rasterizer AA.
- **Icon-font glyphs rendered LOW - ✅ FIXED.** Material Symbols sat 2-4px lower on native
than on jvm, the offset scaling with icon size (~13% of it). Cause: the JVM actual centres
the glyph explicitly (`y = height/2 - (ascent+descent)/2`), while `IconFontIcon` placed
`IconText` in a Box at the DEFAULT TopStart alignment - and the glyph's natural text box is
taller than the nominal icon size. Fixed by centring, with
`wrapContentSize(unbounded = true)` so the text measures at its natural size first:
`Modifier.size()` imposes FIXED constraints, so without it nothing overflows and
contentAlignment is a no-op (a first attempt without it changed literally nothing).
gear/download/lock are now pixel-exact; the eye is 1px off from half-pixel rounding (odd
glyph height centred in an even box; jvm draws at a float y, native placement is integer).
Whole-window >32/255 drift 0.29% -> 0.17%.
- **Missing glyph in the session header.** The leading icon next to "Untitled session"
renders as a `.notdef` TOFU on jvm and as NOTHING on native - so the glyph is absent from
the subsetted Material Symbols font on both, and the two stacks just disagree on how to
draw a missing glyph. The gear icon beside it is fine, so the icon-font path works; this
is almost certainly `enableIconSubsetting` missing a NON-LITERAL `MaterialSymbols.<Name>`
reference that `findMaterialSymbolsUsage` cannot see. Check that call site, and consider
making the native side draw .notdef too so a missing glyph is never silent.
### 7b. Vendor Koin + Coil3 + Pulse MVI (requested 2026-09-12) - ✅ DONE
Target-availability audit against Maven Central (the deciding fact - re-check on every
version bump, and check BOTH the google and jetbrains coordinate sets before concluding an
artifact "has no desktop klibs"):
| Artifact | mingwX64 | linuxX64/arm64 | macosArm64 | Action |
|---|---|---|---|---|
| `androidx.navigationevent:navigationevent-compose` 1.1.2 | YES | YES | YES | **none - already used as-is** |
| `org.jetbrains.androidx.navigationevent:navigationevent-compose` 1.1.0 | YES | YES | YES | **none** |
| `io.insert-koin:koin-core` 4.2.2 | YES | YES | YES | none - Maven as-is |
| `io.insert-koin:koin-core-viewmodel` 4.2.2 | no | no | YES | VENDOR |
| `io.insert-koin:koin-compose` 4.2.2 | no | no | YES | VENDOR |
| `io.insert-koin:koin-compose-viewmodel` 4.2.2 | no | no | YES | VENDOR |
| `io.insert-koin:koin-compose-viewmodel-navigation` 4.2.2 | no | no | YES | VENDOR |
| `io.coil-kt.coil3:coil-core` 3.6.2 | no | YES | YES | VENDOR (mingw gap) |
| `io.coil-kt.coil3:coil` 3.6.2 | no | YES | YES | VENDOR (mingw gap) |
| `io.coil-kt.coil3:coil-network-core` 3.6.2 | no | YES | YES | VENDOR (mingw gap) |
| `io.coil-kt.coil3:coil-network-ktor3` 3.6.2 | no | YES | YES | VENDOR (mingw gap) |
| `io.coil-kt.coil3:coil-svg` 3.6.2 | no | YES | YES | VENDOR (mingw gap) |
| `io.coil-kt.coil3:coil-compose-core` 3.6.2 | no | no | YES | VENDOR |
| `io.coil-kt.coil3:coil-compose` 3.6.2 | no | no | YES | VENDOR |
NOTE the original premise that navigationevent-compose lacks desktop-native artifacts is
WRONG - both coordinate sets publish mingwX64 + linux + macos, and the port already consumes
`androidx.navigationevent:navigationevent-compose:1.1.2` unmodified ("Known Compatible" in
CLAUDE.md, exercised by `demo --backtest`). Nothing to vendor there.
**Landed** as `koin/` (4 modules, `com.bitsycore.koin`), `coil/` (7 modules,
`com.bitsycore.coil3`) and `pulse/` (4 modules, `com.bitsycore.compose.desktop.native.pulse`) - 202 files vendored
verbatim across three new pinned repos (`KOIN_REF=4.2.2`, `COIL_REF=3.6.2`,
`PULSE_REF=0.3.7`), ZERO local source edits. All build for mingwX64 + linuxX64/arm64, are
BCV-dumped, publish, and carry bridge substitution rules so consumers keep declaring the
official coordinates.
Three things worth remembering from doing it:
- **nav2 is a dead end here.** koin-compose-viewmodel-navigation needs nav2's
`navigation-compose`, which has no mingwX64/linux klibs under either coordinate set.
Vendored `koin-compose-navigation3` instead - it needs only koin-compose +
navigation3-runtime, matching this port's nav3 story.
- **Upstream `non*` source sets must stay ABOVE nativeMain, not merge into it.** Several
declare `expect`s whose `actual`s live in upstream nativeMain; collapsing both into one
source set makes them siblings, which is a hard error.
- **A shared-metadata compilation is a real compile.** Coil's nonAndroid code uses
`org.jetbrains.skia.*` directly, so the INTERMEDIATE source set holding it needs skiko on
its own classpath - and since mingwX64 needs the FORK while macOS/Linux need the official
build, the two cannot share an intermediate. coil-core / coil-svg / coil-compose-core
therefore carry two PARALLEL branches over the same srcDirs, the shape
:compose:ui:ui-graphics already used. Kotlin emits no metadata compilation for the
single-target fork branch, so they never collide.
Also hit the granular-metadata trap (CLAUDE.md "Common pitfalls") in coil-compose-core:
every artifact the shared code touches must be declared DIRECTLY and get its own bridge
rule, or `compileCommonMainKotlinMetadata` loses its transitives on the Windows publish job.
REMAINING (deliberate): upstream Koin/Coil/Pulse reference the
`org.jetbrains.androidx.lifecycle:*` mirrors while this port standardises on the GOOGLE
`androidx.lifecycle:*` coords. The build files repoint them, but a consumer that pulls the
jetbrains mirrors in itself will get duplicate `unique_name` klib warnings at metadata
compile. Harmless today; if it ever becomes an error, force the google coords repo-wide.
+162 -25
View File
@@ -1,4 +1,4 @@
[![Kotlin](https://img.shields.io/badge/Kotlin-2.4.0-blue.svg?style=flat&logo=kotlin)](https://kotlinlang.org) [![Kotlin](https://img.shields.io/badge/Kotlin-2.4.20-blue.svg?style=flat&logo=kotlin)](https://kotlinlang.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md)
# Compose Desktop Native # Compose Desktop Native
@@ -18,6 +18,9 @@ executable for macOS (arm64), Linux (x64/arm64), and Windows (mingwX64), using
- **The UI layers are vendored upstream.** `androidx.compose.ui`, `foundation`, - **The UI layers are vendored upstream.** `androidx.compose.ui`, `foundation`,
`animation`, and `material3` are copied from Compose Multiplatform verbatim `animation`, and `material3` are copied from Compose Multiplatform verbatim
wherever they compile as is, with project code filling in only the native glue. wherever they compile as is, with project code filling in only the native glue.
The same treatment extends to ecosystem libraries that stop short of
Kotlin/Native desktop - Koin, Coil 3 and Pulse MVI are vendored and rebuilt for
these targets, and you still declare their official coordinates.
- **Rendering is Skia everywhere.** macOS and Linux link the official Skiko - **Rendering is Skia everywhere.** macOS and Linux link the official Skiko
klibs (Metal / OpenGL); Windows links the bitsycore Skiko fork, which ships klibs (Metal / OpenGL); Windows links the bitsycore Skiko fork, which ships
Skiko and Skia together in `skiko-windows-x64.dll`. Skiko and Skia together in `skiko-windows-x64.dll`.
@@ -30,9 +33,9 @@ executable for macOS (arm64), Linux (x64/arm64), and Windows (mingwX64), using
| Platform | Gradle target | Renderer | | Platform | Gradle target | Renderer |
|----------|---------------|----------| |----------|---------------|----------|
| macOS arm64 | `macosArm64` | Skia (Metal) — official Skiko | | macOS arm64 | `macosArm64` | Skia (Metal) - official Skiko |
| Linux x64 / arm64 | `linuxX64` / `linuxArm64` | Skia (OpenGL) — official Skiko | | Linux x64 / arm64 | `linuxX64` / `linuxArm64` | Skia (OpenGL) - official Skiko |
| Windows | `mingwX64` | Skia — bitsycore Skiko fork | | Windows | `mingwX64` | Skia - bitsycore Skiko fork |
## Quickstart ## Quickstart
@@ -68,13 +71,30 @@ Each window gets its own `Lifecycle`, `ViewModelStore`, and `SavedStateRegistry`
owners, driven by real SDL focus and visibility events, so ViewModels and owners, driven by real SDL focus and visibility events, so ViewModels and
saved state behave as they do on Android. saved state behave as they do on Android.
`Window()` takes the same attributes as Compose Desktop's - `undecorated`,
`transparent`, `resizable`, `enabled`, `focusable`, `alwaysOnTop`,
`onPreviewKeyEvent` and `onKeyEvent` - and re-applies them when they change.
(`transparent` is the one exception: SDL needs it at window-creation time, so it
is fixed for the window's life.)
When the Compose-level API doesn't reach far enough, drop to SDL directly:
```kotlin
val handles = window.rawSdlHandles() // SDL_Window* / SDL_Renderer* / GL / Metal
handles.window?.let { sdl3.SDL_FlashWindow(it.reinterpret(), SDL_FLASH_BRIEFLY) }
```
The pointers are a snapshot valid only while the window lives, and which ones
are non-null depends on the resolved renderer (`glContext` for Skia OpenGL,
`metalView` for Metal, `renderer` for CPU raster).
### Building in this repo ### Building in this repo
```kotlin ```kotlin
commonMain.dependencies { commonMain.dependencies {
implementation(project(":desktop-native-window")) // window shell + main loop implementation(project(":compose:desktop:native:desktop-native-window")) // window shell + main loop
implementation(project(":material3")) // Material 3 widgets implementation(project(":compose:material3:material3")) // Material 3 widgets
implementation(project(":material-symbols")) // icon-font composables (optional) implementation(project(":utils:material-symbols")) // icon-font composables (optional)
} }
``` ```
@@ -82,7 +102,7 @@ commonMain.dependencies {
The klibs publish to [maven.bitsycore.com](https://maven.bitsycore.com/releases) The klibs publish to [maven.bitsycore.com](https://maven.bitsycore.com/releases)
(no auth) and GitHub Packages (authenticated fallback) under per-area coordinates that mirror (no auth) and GitHub Packages (authenticated fallback) under per-area coordinates that mirror
upstream — `com.bitsycore.compose.ui:ui`, `com.bitsycore.compose.foundation:foundation`, upstream - `com.bitsycore.compose.ui:ui`, `com.bitsycore.compose.foundation:foundation`,
… (the `com.bitsycore` fork of each `org.jetbrains.compose.*`). Apply … (the `com.bitsycore` fork of each `org.jetbrains.compose.*`). Apply
the bridge Gradle plugin once, declare the **official** Compose Multiplatform the bridge Gradle plugin once, declare the **official** Compose Multiplatform
coordinates, and the plugin swaps in the port's klibs on native desktop targets coordinates, and the plugin swaps in the port's klibs on native desktop targets
@@ -98,23 +118,29 @@ plugins {
commonMain.dependencies { commonMain.dependencies {
// The plugin exposes the exact Compose versions the port tracks, so you // The plugin exposes the exact Compose versions the port tracks, so you
// never hand-match them (material3 is versioned separately upstream). // never hand-match them (material3 and material3-adaptive each ride their
// own release train upstream).
implementation("org.jetbrains.compose.runtime:runtime:${composeDesktopNative.composeRuntime}") implementation("org.jetbrains.compose.runtime:runtime:${composeDesktopNative.composeRuntime}")
implementation("org.jetbrains.compose.ui:ui:${composeDesktopNative.compose}") implementation("org.jetbrains.compose.ui:ui:${composeDesktopNative.compose}")
implementation("org.jetbrains.compose.foundation:foundation:${composeDesktopNative.compose}") implementation("org.jetbrains.compose.foundation:foundation:${composeDesktopNative.compose}")
implementation("org.jetbrains.compose.material3:material3:${composeDesktopNative.composeMaterial3}") implementation("org.jetbrains.compose.material3:material3:${composeDesktopNative.composeMaterial3}")
// material3-adaptive, which upstream publishes for apple targets only.
// Note adaptive api-exposes androidx.window:window-core - declare it
// DIRECTLY if your commonMain touches WindowSizeClass, since substituted
// modules hide their transitives from the common metadata classpath.
val vAdaptive = composeDesktopNative.composeMaterial3Adaptive
implementation("org.jetbrains.compose.material3.adaptive:adaptive:$vAdaptive")
implementation("org.jetbrains.compose.material3.adaptive:adaptive-layout:$vAdaptive")
implementation("androidx.window:window-core:1.5.0")
} }
``` ```
**Two GitHub Packages repos are needed.** A consumer declares the port's repo **One repository, no credentials.** `https://maven.bitsycore.com/releases`
(`bitsycore/compose-desktop-native`) **and** the skiko fork's (`bitsycore/skiko`). carries both the port's artifacts and the bitsycore skiko fork
The Windows (mingwX64) target renders through the bitsycore skiko fork, and (`com.bitsycore.skiko`), which the Windows (mingwX64) target renders through;
GitHub Packages binds each package name to a **single** repository — so macOS/Linux pull the official skiko from Maven Central instead. The exact
`com.bitsycore.skiko` can't be mirrored into the port's repo, it stays in its `repositories {}` snippet and version pinning:
own. Scope them with `includeGroup`/`excludeGroup("com.bitsycore.skiko")`
(macOS/Linux pull the official skiko from Maven Central instead; if you build no
mingwX64 you can omit the fork repo). The exact `repositories {}` snippet,
credentials, and version pinning:
[gradle-plugin/compose-desktop-native-bridge/README.md](gradle-plugin/compose-desktop-native-bridge/README.md). [gradle-plugin/compose-desktop-native-bridge/README.md](gradle-plugin/compose-desktop-native-bridge/README.md).
For a complete project that applies the bridge and builds one shared UI for For a complete project that applies the bridge and builds one shared UI for
@@ -152,16 +178,123 @@ client certificates.
## Modules ## Modules
One Gradle module per upstream Compose artifact, mirroring the upstream One Gradle module per upstream artifact, and the Gradle path mirrors the
`compose/` tree. The renderer lives in `:ui` and the `sdl3` cinterop in directory (`:compose:ui:ui`, `:compose:foundation:foundation`, …). The renderer
`:sdl-core`; `:desktop-native-window` is the SDL integration layer. `:material-symbols`, `:components-resources`, and lives in `:compose:ui:ui` and the `sdl3` cinterop in `:sdl:sdl-core`;
`:navigation3-ui` are project or vendored modules that the official Maven `:compose:desktop:native:desktop-native-window` is the SDL integration layer.
artifacts do not cover for Kotlin/Native desktop.
Most of the androidx architecture stack (lifecycle, viewmodel, navigation3, Most of the androidx architecture stack (lifecycle, viewmodel, navigation3,
savedstate, navigationevent) ships real Kotlin/Native desktop klibs and runs on savedstate, navigationevent) ships real Kotlin/Native desktop klibs and runs on
the port unmodified. The full module map, dependency graph, and the list of the port unmodified. Where an ecosystem library stops short of Kotlin/Native
compatible artifacts are in [CLAUDE.md](CLAUDE.md). desktop, this repo vendors it verbatim from upstream and rebuilds it for these
targets, so you keep writing against the official coordinates:
| Vendored | Why | Published as |
|----------|-----|--------------|
| Compose `ui` / `foundation` / `animation` / `material3` | the port itself | `com.bitsycore.compose.*` |
| `components-resources`, `navigation3-ui` | no mingwX64 / linux klibs | `com.bitsycore.compose.components`, `com.bitsycore.navigation3` |
| **Koin** viewmodel + compose modules | apple + android only upstream (`koin-core` itself is fine) | `com.bitsycore.koin` |
| **Coil 3** (whole stack) | no mingwX64 anywhere; no desktop native at all for its compose layer | `com.bitsycore.coil3` |
| **Pulse MVI** | no desktop-native artifact upstream | `com.bitsycore.compose.desktop.native.pulse` |
| **material3-adaptive** (all four) | ios + macosArm64 only upstream - no linux, no mingw | `com.bitsycore.compose.material3.adaptive` |
The bridge plugin substitutes each of these on native desktop configurations, so
app code declares `io.insert-koin:koin-compose`, `io.coil-kt.coil3:coil-compose`
and so on exactly as it would anywhere else. The full module map, dependency
graph, and the list of compatible artifacts are in [CLAUDE.md](CLAUDE.md).
## Pinned versions
Everything below is pinned in exactly two files -
[`gradle/libs.versions.toml`](gradle/libs.versions.toml) (Maven coordinates) and
[`scripts/compose-fork/compose.properties`](scripts/compose-fork/compose.properties)
(the upstream git refs the vendored sources are copied from) - plus
[`scripts/build-sdl/build-sdl.properties`](scripts/build-sdl/build-sdl.properties)
for SDL3. A release tracks one Compose Multiplatform version; the table is the
contract.
**Version policy.** Anything Compose Multiplatform integrates is pinned to what
CMP ships - for the androidx mirrors, the *Jetpack* version CMP's release notes
map its `org.jetbrains.androidx.*` artifact to, since this port consumes the
google coordinates. Anything CMP does not integrate is pinned to the latest
release. Deliberate exceptions are commented in the catalog; there is currently
one (`savedstate`, below).
### Toolchain
| | Version | Notes |
|---|---|---|
| Kotlin | **2.4.20** | Newer than the 2.3.20 CMP 1.12.1 is built with. Kotlin/Native consumes older klibs fine, and the whole port builds clean on 2.4.20 with no klib API diff, so there is no reason to hold back. |
| Compose Multiplatform | **1.12.1** | The vendored sources are the `v1.12.1` tag of both upstream repos, and the JVM parity leg forces the same version - no dev-build skew. skiko is unchanged at 0.150.1, so the mingwX64 fork still matches its Skia base. |
| SDL3 | **release-3.4.16** | Built from source as a static lib per host (`scripts/build-sdl/build-all.py`), linked into the executable. |
| Skiko | **0.150.1** | macOS / Linux use the official `org.jetbrains.skiko`. |
| Skiko (Windows fork) | **0.150.1-mingw.2** | `com.bitsycore.skiko:skiko` - Skiko + Skia in `skiko-windows-x64.dll`. Same Skia base as the official build. Public repo, no auth. |
### Compose libraries
The port republishes each vendored artifact under a `com.bitsycore` group that
mirrors the upstream one, so the fork of any given coordinate is obvious. Apply
the bridge plugin and you keep declaring the **official** coordinate.
| Library | Official coordinate | Port coordinate |
|---|---|---|
| Runtime | `org.jetbrains.compose.runtime:runtime*:1.12.1` | *not forked - the official klibs serve every target* |
| UI | `org.jetbrains.compose.ui:ui*:1.12.1` | `com.bitsycore.compose.ui:ui*` |
| Foundation | `org.jetbrains.compose.foundation:foundation*:1.12.1` | `com.bitsycore.compose.foundation:foundation*` |
| Animation | `org.jetbrains.compose.animation:animation*:1.12.1` | `com.bitsycore.compose.animation:animation*` |
| Material Ripple | `org.jetbrains.compose.material:material-ripple:1.12.1` | `com.bitsycore.compose.material:material-ripple` |
| Material3 | `org.jetbrains.compose.material3:material3:1.12.0-alpha03` | `com.bitsycore.compose.material3:material3` |
| Material3 Adaptive | `org.jetbrains.compose.material3.adaptive:adaptive*:1.3.0-rc01` | `com.bitsycore.compose.material3.adaptive:adaptive*` |
| Resources | `org.jetbrains.compose.components:components-resources:1.12.1` | `com.bitsycore.compose.components:components-resources` |
| Navigation3 UI | `org.jetbrains.androidx.navigation3:navigation3-ui` | `com.bitsycore.navigation3:navigation3-ui` |
| Window shell | *no upstream equivalent* | `com.bitsycore.compose:desktop-native-window` |
| SDL layer | *no upstream equivalent* | `com.bitsycore.compose.sdl:sdl-core` |
Material3 rides its own release train upstream: `1.12.0-alpha03` **is** the
version Compose Multiplatform 1.12.1 ships (Jetpack Material3 1.5.0-alpha22).
material3-adaptive rides a third train again.
**material3-adaptive** (`adaptive`, `adaptive-layout`, `adaptive-navigation`,
`adaptive-navigation3`) is published upstream for ios and macosArm64 only - no
linux, no mingw. This port ships all four for every desktop-native target. It
needed no source changes at all: every platform actual the native legs want is
already in upstream's `nonAndroidMain` / `skikoMain` / `nativeMain`, and
`androidx.window:window-core` - the one dependency involved - has published
linux, mingw and macos klibs the whole time. The artifacts were missing only
because upstream never turned the targets on. `demo --screen=Adaptive` renders
a live `ListDetailPaneScaffold`.
### Ecosystem libraries
Vendored because upstream stops short of Kotlin/Native desktop. The bridge
substitutes them on native desktop targets only.
| Library | Official coordinate | Port coordinate | Why |
|---|---|---|---|
| Koin | `io.insert-koin:koin-compose*:4.2.2` | `com.bitsycore.koin:*` | apple + android only upstream |
| Koin core | `io.insert-koin:koin-core:4.2.2` | *not forked* | already ships mingwX64 + linux |
| Coil 3 | `io.coil-kt.coil3:coil*:3.6.3` | `com.bitsycore.coil3:*` | no mingwX64 anywhere; no desktop native at all for the compose layer |
| Pulse MVI | `com.bitsycore.lib:pulse*:0.3.7` | `com.bitsycore.compose.desktop.native.pulse:*` | no desktop-native artifact upstream - and Pulse is itself a `com.bitsycore` library, so the republish is namespaced under this project |
Koin's `koin-compose-viewmodel-navigation` is deliberately **not** provided: it
needs Navigation 2's `navigation-compose`, which has no mingwX64 or linux klibs
under either coordinate set. Use `koin-compose-navigation3`.
### AndroidX, used as-is
These publish real Kotlin/Native desktop klibs and run on the port unmodified -
nothing is vendored or substituted. The port standardises on the **google**
`androidx.*` coordinates; do not also pull the `org.jetbrains.androidx.*`
mirrors or you will have every class twice.
| Library | Coordinate |
|---|---|
| Lifecycle / ViewModel | `androidx.lifecycle:lifecycle-*:2.11.0` |
| SavedState | `androidx.savedstate:savedstate*:1.5.0` (CMP builds against 1.4.0; lifecycle 2.11.0 requires it only as a *minimum*, so 1.5.0 resolves cleanly and gives consumers the newer API) |
| Navigation3 runtime | `androidx.navigation3:navigation3-runtime:1.1.7` |
| Navigation Event | `androidx.navigationevent:navigationevent-compose:1.1.2` |
| Collection | `androidx.collection:collection:1.5.0` |
| Graphics Shapes | `androidx.graphics:graphics-shapes:1.1.0` |
## Building ## Building
@@ -177,6 +310,10 @@ command line tools; Linux needs gcc/g++ and the X11 / Wayland / audio dev
headers; Windows needs a mingw-w64 g++ on PATH. See [TOOLING.md](TOOLING.md) for headers; Windows needs a mingw-w64 g++ on PATH. See [TOOLING.md](TOOLING.md) for
the full build and verification workflow. the full build and verification workflow.
## Support
[Bitsycore's Discord](https://discord.gg/kGnraVAtDR)
## Documentation ## Documentation
- [TOOLING.md](TOOLING.md): building the native libs, vendoring, and verification. - [TOOLING.md](TOOLING.md): building the native libs, vendoring, and verification.
+34 -18
View File
@@ -9,7 +9,7 @@ For architecture, source-set layout, and vendoring rules, see
## Native libraries ## Native libraries
SDL3 is built from source as a static library and linked straight into the SDL3 is built from source as a static library and linked straight into the
executable — the windowing, input, and platform-integration layer. One script executable - the windowing, input, and platform-integration layer. One script
does it on every OS. Output lands in the gitignored `libs/`; the version is does it on every OS. Output lands in the gitignored `libs/`; the version is
pinned in `scripts/build-sdl/build-sdl.properties`. pinned in `scripts/build-sdl/build-sdl.properties`.
@@ -24,11 +24,11 @@ absent). Run it once per machine, or after bumping the pinned version.
The static lib is built **slim**: the port uses SDL only for video/window, The static lib is built **slim**: the port uses SDL only for video/window,
events, clipboard, file dialogs, GL/Metal contexts, the CPU-raster `SDL_Render` events, clipboard, file dialogs, GL/Metal contexts, the CPU-raster `SDL_Render`
blit, filesystem, cursor, locale, theme, and text input/IME. `build-all.py` blit, filesystem, cursor, locale, theme, and text input/IME. `build-all.py`
disables every unused subsystem — audio, joystick, haptic, hidapi, sensor, disables every unused subsystem - audio, joystick, haptic, hidapi, sensor,
power, camera, GPU, offscreen, virtual-joystick (all zero-reference), plus power, camera, GPU, offscreen, virtual-joystick (all zero-reference), plus
tests/examples and the Windows D3D12 driver. If a consumer app ever needs one of tests/examples and the Windows D3D12 driver. If a consumer app ever needs one of
these (e.g. SDL audio), re-enable its `-DSDL_*` flag in `build-all.py` and these (e.g. SDL audio), re-enable its `-DSDL_*` flag in `build-all.py` and
rebuild — a build-time change, no code edit. rebuild - a build-time change, no code edit.
## Vendoring upstream Compose ## Vendoring upstream Compose
@@ -50,13 +50,22 @@ Guardrails that keep the vendor tree honest:
```bash ```bash
python3 scripts/compose-fork/check-vendor-clean.py # src/vendor matches the pinned refs python3 scripts/compose-fork/check-vendor-clean.py # src/vendor matches the pinned refs
python3 scripts/compose-fork/check-vendor-drift.py # hand-edited "manual vendors" match their pin python3 scripts/compose-fork/check-vendor-drift.py # hand-edited "manual vendors" match their pin
python3 scripts/compose-fork/audit-exclusions.py # every `!` exclusion is classified
``` ```
`check-vendor-clean` fails if a hand-edit ever leaks into `src/vendor/`. `check-vendor-clean` fails if a hand-edit ever leaks into `src/vendor/`.
`check-vendor-drift` reads the `// VENDOR-BASE:` header on every edited copy and, `check-vendor-drift` reads the `// VENDOR-BASE:` header on every edited copy and,
using the local upstream clone, reports whether the base actually changed since using the local upstream clone, reports whether the base actually changed since
the pin (needs reconciling) or is merely stale (safe to re-stamp). Run both on the pin (needs reconciling) or is merely stale (safe to re-stamp).
every ref bump.
`audit-exclusions` covers the other half: a manifest `!` line takes a file OUT of
the verbatim sync, and the local replacement is either a derived copy
(`// VENDOR-BASE:`, drift-tracked) or a fresh reimplementation
(`// VENDOR-REIMPL:`, deliberately not tracked - only a SIGNATURE change matters).
It classifies each one, flags any local counterpart carrying NEITHER marker
(invisible to the drift check, silently rotting) and any copy that has become
identical to upstream (drop it and re-vendor), and exits non-zero on an
unclassified file. Run all three on every ref bump.
### Coverage against upstream ### Coverage against upstream
@@ -128,7 +137,7 @@ height]`). Its job is the mac-vs-Windows vertical-spacing question (PLAN.md §1b
the SAME `NotoSans.ttf` bytes go through a different Skia `FontMgr` scaler per the SAME `NotoSans.ttf` bytes go through a different Skia `FontMgr` scaler per
host (CoreText / fontconfig-FreeType / DirectWrite), which can pick different host (CoreText / fontconfig-FreeType / DirectWrite), which can pick different
metric tables. Run it on native AND on the JVM parity app on the SAME host and metric tables. Run it on native AND on the JVM parity app on the SAME host and
diff the lines — Windows-native must match skiko-JVM-Windows. diff the lines - Windows-native must match skiko-JVM-Windows.
```bash ```bash
CDN_TEXT_METRICS=1 demo.kexe --screen=Buttons --screenshot=/tmp/b.bmp CDN_TEXT_METRICS=1 demo.kexe --screen=Buttons --screenshot=/tmp/b.bmp
@@ -145,6 +154,7 @@ demo.kexe --backtest # predictive-back / BackHandler
demo.kexe --clicktest # pointer -> upstream clickable demo.kexe --clicktest # pointer -> upstream clickable
demo.kexe --scrolltest # wheel -> scrollable demo.kexe --scrolltest # wheel -> scrollable
demo.kexe --multiwintest # multi-window lifecycle demo.kexe --multiwintest # multi-window lifecycle
demo.kexe --adaptivetest # material3-adaptive folds/unfolds on a LIVE window resize
demo.kexe --soaktest # memory soak (CDN_SOAK_SCREEN, CDN_SOAK_STATIC, CDN_SOAK_CYCLES) demo.kexe --soaktest # memory soak (CDN_SOAK_SCREEN, CDN_SOAK_STATIC, CDN_SOAK_CYCLES)
``` ```
@@ -160,9 +170,9 @@ gradlew.bat :apidemo:runDebugExecutableMingwX64
``` ```
mingwX64 renders through Skia via the **bitsycore skiko fork**, consumed from mingwX64 renders through Skia via the **bitsycore skiko fork**, consumed from
GitHub Packages as `com.bitsycore.skiko:skiko:0.150.1-mingw.1` (macOS/Linux use the PUBLIC `https://maven.bitsycore.com/releases` (no credentials) as
official Skiko). The runtime `skiko-windows-x64.dll` is auto-provisioned next `com.bitsycore.skiko:skiko:0.150.1-mingw.2` (macOS/Linux use official Skiko). The runtime `skiko-windows-x64.dll` is auto-provisioned next
to the executable by the bridge plugin — no manual copy. The fork itself is to the executable by the bridge plugin - no manual copy. The fork itself is
published by a separate GitHub Actions workflow in the fork repo, out of band published by a separate GitHub Actions workflow in the fork repo, out of band
from this repo's release flow. from this repo's release flow.
@@ -182,10 +192,11 @@ single file to edit; know which axis you are changing.
| Version | Where | Notes | | Version | Where | Notes |
|---------|-------|-------| |---------|-------|-------|
| Project release version | The git tag `vX.Y.Z`. `PUBLISH_VERSION` (from the tag) feeds `vPublishVersion` in `build.gradle.kts`, which strips the leading `v`. Groups mirror upstream per area (`com.bitsycore.compose.<area>:<module>`, e.g. `com.bitsycore.compose.ui:ui`); project-only modules use `com.bitsycore.compose.sdl` and `:desktop-native-window` is `com.bitsycore.compose` — see `groupFor()` in the root build. | Set by the tag, not edited by hand. A non-publish build is `0.0.0-SNAPSHOT`. | | Project release version | The git tag `vX.Y.Z`. `PUBLISH_VERSION` (from the tag) feeds `vPublishVersion` in `build.gradle.kts`, which strips the leading `v`. Groups mirror upstream per area (`com.bitsycore.compose.<area>:<module>`, e.g. `com.bitsycore.compose.ui:ui`); project-only modules use `com.bitsycore.compose.sdl` and `:desktop-native-window` is `com.bitsycore.compose` - see `groupFor()` in the root build. | Set by the tag, not edited by hand. A non-publish build is `0.0.0-SNAPSHOT`. |
| Vendored Compose (native side) | `COMPOSE_CORE_REF` and `COMPOSE_REF` in `scripts/compose-fork/compose.properties`, plus `compose` in `gradle/libs.versions.toml`. | Pin to a durable tag (not a `+dev` commit upstream may GC). Re-sync after changing. | | Vendored Compose (native side) | `COMPOSE_CORE_REF` and `COMPOSE_REF` in `scripts/compose-fork/compose.properties`, plus `compose` in `gradle/libs.versions.toml`. | Pin to a durable tag (not a `+dev` commit upstream may GC). Re-sync after changing. |
| JVM parity forcing | `vComposeJvmVersion` in `demo`, `apidemo`, and `material-symbols` `build.gradle.kts`. | Must be a version PUBLISHED to Maven Central. It may lag the vendored native ref (a documented skew) until the matching version is published. | | Vendored ecosystem libs | `KOIN_REF`, `COIL_REF`, `PULSE_REF` in the same file, plus `koin` / `coil` in `gradle/libs.versions.toml`. | Same rule: durable tags. Each repo gets its own sibling sparse clone (`../cmp-ref-koin`, `../cmp-ref-coil`, `../cmp-ref-pulse-mvi`). Re-sync, then `apiDump` - these modules are BCV-validated like the rest. |
| Skiko | `skiko` in `gradle/libs.versions.toml`. | macOS/Linux use official Skiko (`org.jetbrains.skiko`); mingwX64 uses the bitsycore fork (`com.bitsycore.skiko:skiko:0.150.1-mingw.1` from GitHub Packages), published out of band by the fork repo's own workflow. Must expose the `org.jetbrains.skiko.node` `RenderNode` / `GraphicsContext` API the vendored compose-core uses (the fork keeps upstream's `org.jetbrains.skiko.*` package names; only the Maven coord is rebranded). Verify with a throwaway `skikoRendererMain` compile if unsure. | | JVM parity forcing | `compose` / `composeMaterial3` / `composeRuntime` in `gradle/libs.versions.toml` (read by `demo`, `apidemo`, `material-symbols` and the root build's forcing map). | Must be a version PUBLISHED to Maven Central. It may lag the vendored native ref (a documented skew) until the matching version is published; at v1.12.1 they are in lockstep. |
| Skiko | `skiko` (official) and `skikoMingw` (the fork) in `gradle/libs.versions.toml`; keep the fork's `-mingw.N` base equal to `skiko`. | macOS/Linux use official Skiko (`org.jetbrains.skiko`); mingwX64 uses the bitsycore fork (`com.bitsycore.skiko:skiko:0.150.1-mingw.2` from the public maven.bitsycore.com), published out of band by the fork repo's own workflow. Must expose the `org.jetbrains.skiko.node` `RenderNode` / `GraphicsContext` API the vendored compose-core uses (the fork keeps upstream's `org.jetbrains.skiko.*` package names; only the Maven coord is rebranded). Verify with a throwaway `skikoRendererMain` compile if unsure. |
| SDL3 | `scripts/build-sdl/build-sdl.properties`. | Rebuild `libs/` with `build-all.py` after any change. | | SDL3 | `scripts/build-sdl/build-sdl.properties`. | Rebuild `libs/` with `build-all.py` after any change. |
| Bridge substituted version | Defaults to the bridge plugin's own published version; consumers override with the `composeDesktopNative.version` Gradle property. | The plugin publishes with the release tag, so a consumer on the matching plugin version resolves the right klibs automatically. | | Bridge substituted version | Defaults to the bridge plugin's own published version; consumers override with the `composeDesktopNative.version` Gradle property. | The plugin publishes with the release tag, so a consumer on the matching plugin version resolves the right klibs automatically. |
@@ -193,12 +204,17 @@ single file to edit; know which axis you are changing.
Run this on each upstream bump; it is the flow that keeps the sync tax low. Run this on each upstream bump; it is the flow that keeps the sync tax low.
1. Edit `COMPOSE_CORE_REF` / `COMPOSE_REF` in `compose.properties` (and `compose` 1. Edit the relevant `*_REF` in `compose.properties` - `COMPOSE_CORE_REF` /
in `libs.versions.toml` if the coordinate version moved). `COMPOSE_REF` for Compose, `KOIN_REF` / `COIL_REF` / `PULSE_REF` for the
ecosystem libs (and the matching entry in `libs.versions.toml` if the
coordinate version moved).
2. `scripts/compose-fork/sync.sh` 2. `scripts/compose-fork/sync.sh`
3. `python3 scripts/compose-fork/check-vendor-drift.py`. For any manual vendor 3. `python3 scripts/compose-fork/check-vendor-drift.py`. For any manual vendor
whose upstream base actually changed, reconcile by hand; otherwise re-stamp whose upstream base actually changed, reconcile by hand; otherwise re-stamp
its `// VENDOR-BASE:` header to the new ref. its `// VENDOR-BASE:` header to the new ref. Then
`python3 scripts/compose-fork/audit-exclusions.py` - a newly-identical copy
should go back to verbatim vendoring, and an unclassified one needs a marker.
Finish with `./gradlew apiDump` and review the klib API diff.
4. Build and fix any fallout. 4. Build and fix any fallout.
5. `scripts/verify-mac.sh` (green, including the soak and parity gates). 5. `scripts/verify-mac.sh` (green, including the soak and parity gates).
6. If a matching Compose version is now published to Maven, bump 6. If a matching Compose version is now published to Maven, bump
@@ -216,12 +232,12 @@ Run this on each upstream bump; it is the flow that keeps the sync tax low.
KotlinMultiplatform publications live there; a macOS-only publish leaves the KotlinMultiplatform publications live there; a macOS-only publish leaves the
roots without mingwX64 variants (this bit v0.1.15). roots without mingwX64 variants (this bit v0.1.15).
3. Refresh the klib API baselines: on a Windows host (JDK 21, matching CI) run 3. Refresh the klib API baselines: on a Windows host (JDK 21, matching CI) run
`./gradlew apiDump` and commit any diff. Not a hard gate yet — the API is `./gradlew apiDump` and commit any diff. Not a hard gate yet - the API is
pre-stable and `apiCheck` isn't wired — but regenerating each release keeps pre-stable and `apiCheck` isn't wired - but regenerating each release keeps
the baselines honest so the eventual gate is a no-op. `macosArm64` is inferred the baselines honest so the eventual gate is a no-op. `macosArm64` is inferred
from the linux/mingw ABIs (accurate for the target-independent Compose from the linux/mingw ABIs (accurate for the target-independent Compose
surface); `:sdl-core` (cinterop) and `:material-symbols` (generated icon maps) surface); `:sdl-core` (cinterop) and `:material-symbols` (generated icon maps)
are excluded via `apiValidation.ignoredProjects`. Best host: Windows — it are excluded via `apiValidation.ignoredProjects`. Best host: Windows - it
builds the mingw slice (the fork's unique surface) for real and only infers builds the mingw slice (the fork's unique surface) for real and only infers
macos; the macos ABI is target-independent for the tracked pure-Kotlin modules. macos; the macos ABI is target-independent for the tracked pure-Kotlin modules.
4. `git tag vX.Y.Z && git push origin vX.Y.Z`. This triggers `.github/workflows/publish.yml`. 4. `git tag vX.Y.Z && git push origin vX.Y.Z`. This triggers `.github/workflows/publish.yml`.
+8 -4
View File
@@ -68,7 +68,7 @@ kotlin {
implementation("org.jetbrains.compose.material3:material3:$vComposeM3JvmVersion") implementation("org.jetbrains.compose.material3:material3:$vComposeM3JvmVersion")
// Common Material Symbols Utility // Common Material Symbols Utility
implementation(project(":material-symbols")) implementation(project(":utils:material-symbols"))
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.client.content.negotiation)
@@ -81,13 +81,17 @@ kotlin {
} }
nativeMain { nativeMain {
dependencies { dependencies {
implementation(project(":desktop-native-window")) implementation(project(":compose:desktop:native:desktop-native-window"))
implementation(libs.ktor.client.curl) implementation(libs.ktor.client.curl)
} }
} }
jvmMain { jvmMain {
dependencies { dependencies {
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
// decodeToImageBitmap / decodeToSvgPainter (window icon + in-memory
// image viewer). JVM-only: the native side goes through the port's
// own EncodedImageDecoder seam, so this stays out of commonMain.
implementation("org.jetbrains.compose.components:components-resources:${libs.versions.compose.get()}")
implementation(libs.ktor.client.cio) implementation(libs.ktor.client.cio)
implementation(libs.kotlinx.coroutines.swing) implementation(libs.kotlinx.coroutines.swing)
} }
@@ -127,13 +131,13 @@ compose.desktop.native {
} }
// ================== // ==================
// MARK: Fonts (shared pipeline — buildSrc ComposeFontBundling.kt) // MARK: Fonts (shared pipeline - buildSrc ComposeFontBundling.kt)
// ================== // ==================
registerComposeFontBundling { registerComposeFontBundling {
bundleNotoSans = true bundleNotoSans = true
// The mono body font is loaded through the app's own seam (Fonts.kt), not via // The mono body font is loaded through the app's own seam (Fonts.kt), not via
// FontFamily.Monospace call sites — auto-detection can't see it, bundle explicitly. // FontFamily.Monospace call sites - auto-detection can't see it, bundle explicitly.
bundleNotoSansMono = true bundleNotoSansMono = true
bundleMaterialSymbols = true bundleMaterialSymbols = true
enableIconSubsetting = true enableIconSubsetting = true
@@ -9,7 +9,7 @@ import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
// macOS: Ktor bundles an OpenSSL-backed libcurl, which reads PEM / DER / PKCS#12 // macOS: Ktor bundles an OpenSSL-backed libcurl, which reads PEM / DER / PKCS#12
// certificate and key files directly — no certificate-store dance needed. // certificate and key files directly - no certificate-store dance needed.
/** Point libcurl straight at the certificate / key files. */ /** Point libcurl straight at the certificate / key files. */
actual fun prepareClientCert(inReq: ApiRequest): PreparedCert = actual fun prepareClientCert(inReq: ApiRequest): PreparedCert =
@@ -26,7 +26,7 @@ actual fun prepareClientCert(inReq: ApiRequest): PreparedCert =
actual fun sweepTempClientCerts() {} actual fun sweepTempClientCerts() {}
/** Continue the server's chain with intermediates / roots pulled from the /** Continue the server's chain with intermediates / roots pulled from the
macOS Keychain — same UX as the Windows path does via the trust store. macOS Keychain - same UX as the Windows path does via the trust store.
Falls back to the name-only placeholder if Apple's APIs can't resolve Falls back to the name-only placeholder if Apple's APIs can't resolve
the chain (e.g. the server's leaf doesn't parse, or its issuer isn't the chain (e.g. the server's leaf doesn't parse, or its issuer isn't
in any keychain). */ in any keychain). */
@@ -43,7 +43,7 @@ actual fun extendChain(inServerCerts: List<List<Pair<String, String>>>): List<Ch
// ============ // ============
/** Build the chain by handing the leaf to SecTrust + evaluating against /** Build the chain by handing the leaf to SecTrust + evaluating against
an SSL policy — Apple then walks Keychain, pulling intermediates and an SSL policy - Apple then walks Keychain, pulling intermediates and
the root in. Returns the resolved chain with fromServer=true for the the root in. Returns the resolved chain with fromServer=true for the
first inServerCount entries (those came down on the wire) and false first inServerCount entries (those came down on the wire) and false
for anything the OS added. */ for anything the OS added. */
@@ -53,7 +53,7 @@ private fun osChainApple(
inServerCount: Int, inServerCount: Int,
inServerCerts: List<List<Pair<String, String>>>, inServerCerts: List<List<Pair<String, String>>>,
): List<ChainCert> { ): List<ChainCert> {
// Parse every cert the server presented, not just the leaf — Apple's // Parse every cert the server presented, not just the leaf - Apple's
// chain builder uses them as candidate intermediates when stitching to // chain builder uses them as candidate intermediates when stitching to
// a root, instead of relying solely on AIA-fetch which is off by default. // a root, instead of relying solely on AIA-fetch which is off by default.
val vServerCerts = inServerCerts.mapNotNull { val vServerCerts = inServerCerts.mapNotNull {
@@ -82,7 +82,7 @@ private fun osChainApple(
} }
val vTrust = vTrustVar.value!! val vTrust = vTrustVar.value!!
// Evaluate even if it fails — we still want the partial chain back so // Evaluate even if it fails - we still want the partial chain back so
// the UI can show what it could resolve. SecTrustEvaluateWithError // the UI can show what it could resolve. SecTrustEvaluateWithError
// stops at the highest cert it found. // stops at the highest cert it found.
val vErr = nativeHeap.alloc<CFErrorRefVar>() val vErr = nativeHeap.alloc<CFErrorRefVar>()
@@ -137,7 +137,7 @@ private fun createSecCertificate(inDer: ByteArray): SecCertificateRef? {
return vCert return vCert
} }
/** CFArray of SecCertificateRef — the input to SecTrustCreateWithCertificates. */ /** CFArray of SecCertificateRef - the input to SecTrustCreateWithCertificates. */
@OptIn(ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
private fun createCFArray(inCerts: List<SecCertificateRef>): CFArrayRef { private fun createCFArray(inCerts: List<SecCertificateRef>): CFArrayRef {
val vPtrs = nativeHeap.allocArray<COpaquePointerVar>(inCerts.size) val vPtrs = nativeHeap.allocArray<COpaquePointerVar>(inCerts.size)
@@ -147,7 +147,7 @@ private fun createCFArray(inCerts: List<SecCertificateRef>): CFArrayRef {
return vArr return vArr
} }
/** Apple's "subject summary" — basically the CN, but falls back to the /** Apple's "subject summary" - basically the CN, but falls back to the
organisation when no CN is present (matches what Keychain Access organisation when no CN is present (matches what Keychain Access
displays in its cert list). */ displays in its cert list). */
@OptIn(ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
@@ -157,7 +157,7 @@ private fun secSubjectSummary(inCert: SecCertificateRef): String? {
return vNs?.toString() return vNs?.toString()
} }
/** PEM string for a SecCertificate — base64 of the DER bytes wrapped /** PEM string for a SecCertificate - base64 of the DER bytes wrapped
with -----BEGIN CERTIFICATE-----/-----END----- headers. */ with -----BEGIN CERTIFICATE-----/-----END----- headers. */
@OptIn(ExperimentalForeignApi::class, ExperimentalEncodingApi::class) @OptIn(ExperimentalForeignApi::class, ExperimentalEncodingApi::class)
private fun secCertPem(inCert: SecCertificateRef): String? { private fun secCertPem(inCert: SecCertificateRef): String? {
+28 -22
View File
@@ -87,7 +87,7 @@ internal fun App() {
// Focus the saved active pack, but fall back to one that actually has tabs so // Focus the saved active pack, but fall back to one that actually has tabs so
// the strip shows whenever any tab is open. // the strip shows whenever any tab is open.
// The active pack/scope as a reference (a top-level pack, a sub-pack, or the // The active pack/scope as a reference (a top-level pack, a sub-pack, or the
// loose root) — a reference (not an index) so sub-packs can be active. // loose root) - a reference (not an index) so sub-packs can be active.
var vActivePackRef by remember { var vActivePackRef by remember {
mutableStateOf( mutableStateOf(
vBoot.activePack.coerceIn(0, (vPacks.size - 1).coerceAtLeast(0)).let { vIdx -> vBoot.activePack.coerceIn(0, (vPacks.size - 1).coerceAtLeast(0)).let { vIdx ->
@@ -138,7 +138,7 @@ internal fun App() {
// Variables a request sees: session, then each enclosing pack (inner overrides). // Variables a request sees: session, then each enclosing pack (inner overrides).
fun effective(inP: PackState): List<KeyVal> = vGlobalEnv.toList() + scopeChain(inP).flatMap { it.variables } fun effective(inP: PackState): List<KeyVal> = vGlobalEnv.toList() + scopeChain(inP).flatMap { it.variables }
// …plus the request's own variables (innermost — they win over everything above). // …plus the request's own variables (innermost - they win over everything above).
fun effectiveReqVars(inReq: ApiRequest, inP: PackState): List<KeyVal> = effective(inP) + inReq.variables fun effectiveReqVars(inReq: ApiRequest, inP: PackState): List<KeyVal> = effective(inP) + inReq.variables
// Query params a request inherits: session, then each enclosing pack (inner wins by key). // Query params a request inherits: session, then each enclosing pack (inner wins by key).
@@ -192,7 +192,7 @@ internal fun App() {
// the tooltip. inChain for a request = scopeChain(owningPack) (its own pack // the tooltip. inChain for a request = scopeChain(owningPack) (its own pack
// included); for a pack's settings = scopeChain(pack.parent) (ancestors only). // included); for a pack's settings = scopeChain(pack.parent) (ancestors only).
// The full path of a scope for the source tooltip: "Methods" for a top-level // The full path of a scope for the source tooltip: "Methods" for a top-level
// pack, "Methods / Nested" for a sub-pack (root excluded — it's never inherited). // pack, "Methods / Nested" for a sub-pack (root excluded - it's never inherited).
fun scopePath(inP: PackState): String { fun scopePath(inP: PackState): String {
val vParts = ArrayList<String>() val vParts = ArrayList<String>()
var vCur: PackState? = inP var vCur: PackState? = inP
@@ -243,7 +243,7 @@ internal fun App() {
val vGE = vGlobalEnv.toList() val vGE = vGlobalEnv.toList()
val vRootPack = vRoot.toPack() val vRootPack = vRoot.toPack()
val vRootTabs = vRoot.openTabs.mapNotNull { vRs -> vRoot.requests.indexOf(vRs).takeIf { it >= 0 } } val vRootTabs = vRoot.openTabs.mapNotNull { vRs -> vRoot.requests.indexOf(vRs).takeIf { it >= 0 } }
// Open-tab state — persisted in app state only, not in the session file. // Open-tab state - persisted in app state only, not in the session file.
val vOpen = vPacks.map { vP -> vP.openTabs.mapNotNull { vRs -> vP.requests.indexOf(vRs).takeIf { it >= 0 } } } val vOpen = vPacks.map { vP -> vP.openTabs.mapNotNull { vRs -> vP.requests.indexOf(vRs).takeIf { it >= 0 } } }
val vActiveReq = vActivePackRef?.let { it.requests.indexOf(it.active) } ?: -1 val vActiveReq = vActivePackRef?.let { it.requests.indexOf(it.active) } ?: -1
saveAppState( saveAppState(
@@ -266,7 +266,7 @@ internal fun App() {
) )
) )
// A session opened from / saved to a file auto-saves back to it on every // A session opened from / saved to a file auto-saves back to it on every
// change — once it has a file, it's always in sync (best-effort). The // change - once it has a file, it's always in sync (best-effort). The
// Session has no open-tab fields, so the file never carries them. // Session has no open-tab fields, so the file never carries them.
vSessionPath?.let { vSessionPath?.let {
exportSession( exportSession(
@@ -315,7 +315,7 @@ internal fun App() {
} }
// Open a request in a specific pack's context (a linked copy shares the // Open a request in a specific pack's context (a linked copy shares the
// source's ReqState objects, so the pack must be passed — packOf would // source's ReqState objects, so the pack must be passed - packOf would
// resolve to the source, not the linked copy). // resolve to the source, not the linked copy).
fun open(inRs: ReqState, inPack: PackState) { fun open(inRs: ReqState, inPack: PackState) {
if (inRs !in inPack.openTabs) inPack.openTabs.add(inRs) if (inRs !in inPack.openTabs) inPack.openTabs.add(inRs)
@@ -378,7 +378,7 @@ internal fun App() {
val vTab = vFlat.getOrNull(inFrom) ?: return val vTab = vFlat.getOrNull(inFrom) ?: return
val vRs = vTab.req ?: return val vRs = vTab.req ?: return
val vP = vTab.pack ?: return val vP = vTab.pack ?: return
// Flat index of this pack's first request tab — works for the root and any // Flat index of this pack's first request tab - works for the root and any
// (nested) pack without hand-computing offsets. // (nested) pack without hand-computing offsets.
val vReqStart = vFlat.indexOfFirst { it.pack === vP && it.req != null } val vReqStart = vFlat.indexOfFirst { it.pack === vP && it.req != null }
if (vReqStart < 0) return if (vReqStart < 0) return
@@ -400,7 +400,7 @@ internal fun App() {
vP.requests.add(vRs); vP.openTabs.add(vRs); vP.active = vRs; vP.dirty = true; vReqMsg = null; vSideTab = 0 vP.requests.add(vRs); vP.openTabs.add(vRs); vP.active = vRs; vP.dirty = true; vReqMsg = null; vSideTab = 0
} }
// A loose request at the session root (no pack — inherits session settings only). // A loose request at the session root (no pack - inherits session settings only).
fun newLooseRequest() { fun newLooseRequest() {
val vRs = ReqState(ApiRequest(name = "Request ${vRoot.requests.size + 1}")) val vRs = ReqState(ApiRequest(name = "Request ${vRoot.requests.size + 1}"))
vRoot.requests.add(vRs); vRoot.openTabs.add(vRs); vRoot.active = vRs vRoot.requests.add(vRs); vRoot.openTabs.add(vRs); vRoot.active = vRs
@@ -548,7 +548,7 @@ internal fun App() {
} }
// A linked copy mirrors inP's requests read-only but gets its own (copied) // A linked copy mirrors inP's requests read-only but gets its own (copied)
// variables / headers / cert — for running the same calls against another env. // variables / headers / cert - for running the same calls against another env.
fun createLinkedPack(inP: PackState) { fun createLinkedPack(inP: PackState) {
val vSource = inP.linkedSource ?: inP // link to the real source, never to another link val vSource = inP.linkedSource ?: inP // link to the real source, never to another link
val vAt = (vPacks.indexOf(inP) + 1).coerceIn(0, vPacks.size) val vAt = (vPacks.indexOf(inP) + 1).coerceIn(0, vPacks.size)
@@ -582,7 +582,7 @@ internal fun App() {
// Move inRs out of inFrom into inTo at inIndex. inIndex counts inTo's rows // Move inRs out of inFrom into inTo at inIndex. inIndex counts inTo's rows
// *excluding* the dragged request (so for a same-pack reorder it is already an // *excluding* the dragged request (so for a same-pack reorder it is already an
// index into the post-removal list — no shift needed; resolveReqDrop produces // index into the post-removal list - no shift needed; resolveReqDrop produces
// exactly this). A request that was open / active follows to the new pack so // exactly this). A request that was open / active follows to the new pack so
// its tab survives the move. // its tab survives the move.
fun moveRequest(inRs: ReqState, inFrom: PackState, inTo: PackState, inIndex: Int) { fun moveRequest(inRs: ReqState, inFrom: PackState, inTo: PackState, inIndex: Int) {
@@ -746,7 +746,7 @@ internal fun App() {
} }
// Commit the resolved request / pack drop, then clear the drag. A press that // Commit the resolved request / pack drop, then clear the drag. A press that
// never passed the slop (engaged == false) is a click, not a drop — skip it. // never passed the slop (engaged == false) is a click, not a drop - skip it.
fun reqDropEnd() { fun reqDropEnd() {
val vRs = vTreeDrag.dragReq val vRs = vTreeDrag.dragReq
val vFrom = vTreeDrag.dragReqOwner val vFrom = vTreeDrag.dragReqOwner
@@ -915,7 +915,13 @@ internal fun App() {
val vExtended = if (vDark) vPalette.scheme.darkExtended else vPalette.scheme.lightExtended val vExtended = if (vDark) vPalette.scheme.darkExtended else vPalette.scheme.lightExtended
val vC = appColorsFromScheme(vScheme) val vC = appColorsFromScheme(vScheme)
MaterialTheme(colorScheme = vScheme) { // PARITY: pin the default text family to the bundled NotoSans on BOTH stacks.
// Left alone, native resolves FontFamily.Default to the bundled font while the
// JVM leg resolves it to a system face, so every un-familied Text was being
// compared against a different typeface (see Fonts.kt).
val vTypography = MaterialTheme.typography.withDefaultFontFamily(defaultFontFamily)
MaterialTheme(colorScheme = vScheme, typography = vTypography) {
CompositionLocalProvider( CompositionLocalProvider(
LocalAppColors provides vC, LocalAppColors provides vC,
LocalVolticExtended provides vExtended, LocalVolticExtended provides vExtended,
@@ -933,7 +939,7 @@ internal fun App() {
// ============ // ============
// Sidebar (Pack panel) // Sidebar (Pack panel)
Column(modifier = Modifier.fillMaxSize().background(vC.panel)) { Column(modifier = Modifier.fillMaxSize().background(vC.panel)) {
// Sticky header — pack switcher + section tabs stay pinned while the list scrolls. // Sticky header - pack switcher + section tabs stay pinned while the list scrolls.
Column( Column(
modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 12.dp, bottom = 8.dp), modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 12.dp, bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -1039,7 +1045,7 @@ internal fun App() {
} }
if (vPacks.isEmpty() && vRoot.requests.isEmpty()) { if (vPacks.isEmpty() && vRoot.requests.isEmpty()) {
Text( Text(
"Nothing here yet — use Add (+) for a request or pack, or Open above.", "Nothing here yet - use Add (+) for a request or pack, or Open above.",
color = vC.dim, color = vC.dim,
fontSize = 12.sp fontSize = 12.sp
) )
@@ -1140,7 +1146,7 @@ internal fun App() {
}, },
second = { second = {
// ============ // ============
// Main — unified tab strip over the editor (request) or pack env. // Main - unified tab strip over the editor (request) or pack env.
val vTabs = stripTabs() val vTabs = stripTabs()
val vReqActive = vP?.active val vReqActive = vP?.active
val vEnvShown = vEnvActive && vP != null && vP.envOpen val vEnvShown = vEnvActive && vP != null && vP.envOpen
@@ -1161,12 +1167,12 @@ internal fun App() {
"Load default session" "Load default session"
) { loadDefaultSession() } ) { loadDefaultSession() }
} }
} else Text("Nothing open — click a request, a pack, or Session settings.", color = vC.dim) } else Text("Nothing open - click a request, a pack, or Session settings.", color = vC.dim)
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
} }
} else { } else {
Column(modifier = Modifier.fillMaxSize().background(vC.bg)) { Column(modifier = Modifier.fillMaxSize().background(vC.bg)) {
// Panel 1 — unified tab strip (session + pack-settings + request tabs). // Panel 1 - unified tab strip (session + pack-settings + request tabs).
RequestTabStrip( RequestTabStrip(
inTabs = vTabs, inTabs = vTabs,
inActiveKey = when { inActiveKey = when {
@@ -1263,7 +1269,7 @@ internal fun App() {
inOnCert = { vP.cert = it; vP.dirty = true; persist() }, inOnCert = { vP.cert = it; vP.dirty = true; persist() },
inCertHelp = "Used by every request in this pack unless the request sets its own. Overrides the session cert.", inCertHelp = "Used by every request in this pack unless the request sets its own. Overrides the session cert.",
inCertHeading = "Pack client certificate", inCertHeading = "Pack client certificate",
// What this pack inherits from above (session + ancestor packs — itself excluded). // What this pack inherits from above (session + ancestor packs - itself excluded).
inInheritedVars = sourcedVars(scopeChain(vP.parent)), inInheritedVars = sourcedVars(scopeChain(vP.parent)),
inInheritedParams = sourcedParams(scopeChain(vP.parent)), inInheritedParams = sourcedParams(scopeChain(vP.parent)),
inInheritedHeaders = sourcedHeaders(scopeChain(vP.parent)), inInheritedHeaders = sourcedHeaders(scopeChain(vP.parent)),
@@ -1286,13 +1292,13 @@ internal fun App() {
size = 14.dp size = 14.dp
) )
Text( Text(
"Linked copy — read-only. Runs with this pack's Var/Header/Cert; edit the request in “${vP.linkedSource?.name ?: "source"}”.", "Linked copy - read-only. Runs with this pack's Var/Header/Cert; edit the request in “${vP.linkedSource?.name ?: "source"}”.",
color = vC.dim, color = vC.dim,
fontSize = 11.sp fontSize = 11.sp
) )
} }
} }
// Panel 2 — unified method · url · send. // Panel 2 - unified method · url · send.
UrlBar( UrlBar(
inReq = vReq, inReq = vReq,
inLoading = vReqActive.loading, inLoading = vReqActive.loading,
@@ -1438,7 +1444,7 @@ internal fun App() {
Text("Replace current session?", color = vC.text, fontSize = 16.sp) Text("Replace current session?", color = vC.text, fontSize = 16.sp)
} }
Text( Text(
"This session hasn't been saved to a file — continuing will discard it. Save it first if you want to keep it.", "This session hasn't been saved to a file - continuing will discard it. Save it first if you want to keep it.",
color = vC.dim, color = vC.dim,
fontSize = 13.sp fontSize = 13.sp
) )
@@ -1549,7 +1555,7 @@ internal fun App() {
Text("Remove pack", color = vC.text, fontSize = 16.sp) Text("Remove pack", color = vC.text, fontSize = 16.sp)
} }
Text( Text(
"\"${vRmPack.name}\" will be removed from the session. Unsaved changes are lost — export it first to keep them.", "\"${vRmPack.name}\" will be removed from the session. Unsaved changes are lost - export it first to keep them.",
color = vC.dim, color = vC.dim,
fontSize = 13.sp fontSize = 13.sp
) )
@@ -40,9 +40,9 @@ internal fun imageFileName(inContentType: String?): String = when {
} }
internal fun statusColor(inStatus: Int): Color = when (inStatus) { internal fun statusColor(inStatus: Int): Color = when (inStatus) {
in 200..299 -> Color(0xFF36B37E) // success — green in 200..299 -> Color(0xFF36B37E) // success - green
in 300..399 -> Color(0xFF4C9AFF) // redirect — blue in 300..399 -> Color(0xFF4C9AFF) // redirect - blue
in 400..499 -> Color(0xFFFF991F) // client error — orange (warning) in 400..499 -> Color(0xFFFF991F) // client error - orange (warning)
in 500..599 -> Color(0xFFFF5630) // server error — red in 500..599 -> Color(0xFFFF5630) // server error - red
else -> Color(0xFFFF991F) // unknown / pending — orange else -> Color(0xFFFF991F) // unknown / pending - orange
} }
+6 -6
View File
@@ -15,7 +15,7 @@ internal class HistoryEntry(
val request: ApiRequest val request: ApiRequest
) )
/** One request plus its live session state — response, in-flight job (for /** One request plus its live session state - response, in-flight job (for
cancel) and which sub-tabs show. Stable identity lets the sidebar and the cancel) and which sub-tabs show. Stable identity lets the sidebar and the
open-tab strip reference the same request without index juggling. */ open-tab strip reference the same request without index juggling. */
internal class ReqState(inInitial: ApiRequest) { internal class ReqState(inInitial: ApiRequest) {
@@ -56,11 +56,11 @@ internal class PackState(
// A linked copy mirrors its source's requests read-only; non-linked packs own // A linked copy mirrors its source's requests read-only; non-linked packs own
// their requests. `requests` resolves to whichever applies. // their requests. `requests` resolves to whichever applies.
var linkedSource by mutableStateOf<PackState?>(null) var linkedSource by mutableStateOf<PackState?>(null)
private val fOwnRequests = mutableStateListOf<ReqState>().apply { private val mOwnRequests = mutableStateListOf<ReqState>().apply {
inPack.requests.forEach { add(ReqState(it)) } // packs (and the loose root) may be empty inPack.requests.forEach { add(ReqState(it)) } // packs (and the loose root) may be empty
} }
val requests: androidx.compose.runtime.snapshots.SnapshotStateList<ReqState> val requests: androidx.compose.runtime.snapshots.SnapshotStateList<ReqState>
get() = linkedSource?.requests ?: fOwnRequests get() = linkedSource?.requests ?: mOwnRequests
val isLinked: Boolean get() = linkedSource != null val isLinked: Boolean get() = linkedSource != null
var parent: PackState? = var parent: PackState? =
null // enclosing pack (null = top-level / root) null // enclosing pack (null = top-level / root)
@@ -82,7 +82,7 @@ internal class PackState(
inOpenTabs.forEach { vIdx -> requests.getOrNull(vIdx)?.let { add(it) } } inOpenTabs.forEach { vIdx -> requests.getOrNull(vIdx)?.let { add(it) } }
} }
var active by mutableStateOf(requests.getOrNull(inActive)?.takeIf { it in openTabs } ?: openTabs.firstOrNull()) var active by mutableStateOf(requests.getOrNull(inActive)?.takeIf { it in openTabs } ?: openTabs.firstOrNull())
var expanded by mutableStateOf(true) // sidebar fold state (transient — not part of the pack file) var expanded by mutableStateOf(true) // sidebar fold state (transient - not part of the pack file)
var envOpen by mutableStateOf(false) // whether this pack's env tab is open in the strip (transient) var envOpen by mutableStateOf(false) // whether this pack's env tab is open in the strip (transient)
fun toPack(): Pack = Pack( fun toPack(): Pack = Pack(
@@ -112,7 +112,7 @@ internal const val kSessionTabKey = "session-settings"
internal class StripTab(val pack: PackState?, val req: ReqState?, val isSession: Boolean = false) internal class StripTab(val pack: PackState?, val req: ReqState?, val isSession: Boolean = false)
/** Identity of a strip tab. A linked pack shares the source's ReqState objects, /** Identity of a strip tab. A linked pack shares the source's ReqState objects,
so the key must include the pack — otherwise the source's and linked's tabs so the key must include the pack - otherwise the source's and linked's tabs
for the same request collide (double selection, duplicate key()). Data class → for the same request collide (double selection, duplicate key()). Data class →
structural equality, so compare keys with == (not ===). */ structural equality, so compare keys with == (not ===). */
internal data class TabKey(val pack: PackState?, val req: ReqState?) internal data class TabKey(val pack: PackState?, val req: ReqState?)
@@ -138,7 +138,7 @@ internal class TreeDrag {
var dy by mutableStateOf(0f) // draw-only follow offset for the grabbed element var dy by mutableStateOf(0f) // draw-only follow offset for the grabbed element
var pressRel by mutableStateOf(0) // relY at capture (cursor = grabbed-element top + relY) var pressRel by mutableStateOf(0) // relY at capture (cursor = grabbed-element top + relY)
// A press doesn't become a drag until the pointer moves past kDragSlop — so a // A press doesn't become a drag until the pointer moves past kDragSlop - so a
// plain click (which carries a pixel or two of jitter) still selects / opens. // plain click (which carries a pixel or two of jitter) still selects / opens.
var engaged by mutableStateOf(false) var engaged by mutableStateOf(false)
+3 -3
View File
@@ -6,7 +6,7 @@ import kotlin.io.encoding.Base64
// MARK: Minimal X.509 / DER parser // MARK: Minimal X.509 / DER parser
// ================== // ==================
// Some TLS backends (notably Schannel on Windows) expose only Subject / Issuer / // Some TLS backends (notably Schannel on Windows) expose only Subject / Issuer /
// the PEM through CURLINFO_CERTINFO — not the parsed fields (validity, serial, // the PEM through CURLINFO_CERTINFO - not the parsed fields (validity, serial,
// SAN, algorithms). Since every backend gives us the PEM, we parse it ourselves // SAN, algorithms). Since every backend gives us the PEM, we parse it ourselves
// to fill in the detail. Just enough ASN.1 to walk a Certificate; anything odd // to fill in the detail. Just enough ASN.1 to walk a Certificate; anything odd
// is swallowed and the field is simply omitted. // is swallowed and the field is simply omitted.
@@ -94,7 +94,7 @@ private fun pemToDer(inPem: String): ByteArray? {
} }
} }
/** The friendly name (or dotted OID) of the first child OID of a SEQUENCE — used /** The friendly name (or dotted OID) of the first child OID of a SEQUENCE - used
for signatureAlgorithm and subjectPublicKeyInfo's AlgorithmIdentifier. */ for signatureAlgorithm and subjectPublicKeyInfo's AlgorithmIdentifier. */
private fun firstOidName(inB: ByteArray, inSeq: IntArray): String? { private fun firstOidName(inB: ByteArray, inSeq: IntArray): String? {
val vR = DerReader(inB, inSeq[1], inSeq[2]) val vR = DerReader(inB, inSeq[1], inSeq[2])
@@ -139,7 +139,7 @@ private fun parseAsn1Time(inB: ByteArray, inTlv: IntArray): String? {
if (inTlv[0] == 0x18) // GeneralizedTime if (inTlv[0] == 0x18) // GeneralizedTime
{ {
vYear = vS.substring(0, 4); vRest = vS.substring(4) vYear = vS.substring(0, 4); vRest = vS.substring(4)
} else // UTCTime — RFC 5280 pivot at 50 } else // UTCTime - RFC 5280 pivot at 50
{ {
val vYy = vS.substring(0, 2).toInt() val vYy = vS.substring(0, 2).toInt()
vYear = (if (vYy >= 50) "19" else "20") + vS.substring(0, 2) vYear = (if (vYy >= 50) "19" else "20") + vS.substring(0, 2)
+49 -1
View File
@@ -1,5 +1,6 @@
package apidemo package apidemo
import androidx.compose.material3.Typography
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
// ================== // ==================
@@ -11,10 +12,57 @@ import androidx.compose.ui.text.font.FontFamily
// back to the default proportional font. // back to the default proportional font.
const val kMonoFamily = "noto-mono" const val kMonoFamily = "noto-mono"
/** The raw family name string — used by wrappedRowCount, which measures via the /** Family name the bundled proportional font registers under. */
const val kDefaultFamily = "noto-sans"
/** The raw family name string - used by wrappedRowCount, which measures via the
platform text pipeline (accepts a name string). Null when not bundled. */ platform text pipeline (accepts a name string). Null when not bundled. */
expect val monoFontFamilyName: String? expect val monoFontFamilyName: String?
/** The material3 Text / BasicTextField-shaped FontFamily for the mono family. /** The material3 Text / BasicTextField-shaped FontFamily for the mono family.
Null when not bundled. */ Null when not bundled. */
expect val monoFontFamily: FontFamily? expect val monoFontFamily: FontFamily?
// ==================
// MARK: Default proportional font (seam)
// ==================
// PARITY: the native side resolves FontFamily.Default to the BUNDLED NotoSans
// (SkiaFonts.defaultTypeface is the fallback for every unresolved family), while
// stock Compose Desktop on the JVM resolves it to a SYSTEM font (Segoe UI on
// Windows, Helvetica on macOS). Different typefaces mean different ascent /
// descent / line-gap, so identical layout code puts glyphs at a different height
// inside an identically-sized text box - which is what made the SourceTag pill
// look vertically centred on JVM and off-centre on native.
//
// The two stacks must rasterise the SAME face for the comparison to mean
// anything, so the app pins its default family explicitly rather than inheriting
// whatever the platform picks. Null = the bundled font isn't available, in which
// case we fall back to the platform default and the skew returns.
/** The bundled proportional font, applied app-wide as the default text family.
Null when NotoSans isn't shipped (data.kres on native, classpath font/ on jvm). */
expect val defaultFontFamily: FontFamily?
/** Re-stamps every style in a Material 3 [Typography] with [inFamily]. M3 seeds
LocalTextStyle from the typography, so this reaches every `Text` that doesn't name
a family of its own. A null family leaves the typography untouched. */
fun Typography.withDefaultFontFamily(inFamily: FontFamily?): Typography {
if (inFamily == null) return this
return copy(
displayLarge = displayLarge.copy(fontFamily = inFamily),
displayMedium = displayMedium.copy(fontFamily = inFamily),
displaySmall = displaySmall.copy(fontFamily = inFamily),
headlineLarge = headlineLarge.copy(fontFamily = inFamily),
headlineMedium = headlineMedium.copy(fontFamily = inFamily),
headlineSmall = headlineSmall.copy(fontFamily = inFamily),
titleLarge = titleLarge.copy(fontFamily = inFamily),
titleMedium = titleMedium.copy(fontFamily = inFamily),
titleSmall = titleSmall.copy(fontFamily = inFamily),
bodyLarge = bodyLarge.copy(fontFamily = inFamily),
bodyMedium = bodyMedium.copy(fontFamily = inFamily),
bodySmall = bodySmall.copy(fontFamily = inFamily),
labelLarge = labelLarge.copy(fontFamily = inFamily),
labelMedium = labelMedium.copy(fontFamily = inFamily),
labelSmall = labelSmall.copy(fontFamily = inFamily),
)
}
+3 -3
View File
@@ -11,7 +11,7 @@ import kotlinx.serialization.json.JsonElement
// respects the editor's "tab size" (TextLayoutConfig.tabWidth), exactly like // respects the editor's "tab size" (TextLayoutConfig.tabWidth), exactly like
// typed tabs. Lenient so slightly-off JSON (single quotes, trailing commas) // typed tabs. Lenient so slightly-off JSON (single quotes, trailing commas)
// still reformats instead of failing. // still reformats instead of failing.
private val fPrettyJson = Json { private val mPrettyJson = Json {
prettyPrint = true prettyPrint = true
prettyPrintIndent = "\t" prettyPrintIndent = "\t"
isLenient = true isLenient = true
@@ -23,7 +23,7 @@ private val fPrettyJson = Json {
fun formatBody(inText: String, inFormat: BodyFormat): String = fun formatBody(inText: String, inFormat: BodyFormat): String =
when (inFormat) { when (inFormat) {
BodyFormat.JSON -> runCatching { BodyFormat.JSON -> runCatching {
fPrettyJson.encodeToString(JsonElement.serializer(), fPrettyJson.parseToJsonElement(inText)) mPrettyJson.encodeToString(JsonElement.serializer(), mPrettyJson.parseToJsonElement(inText))
}.getOrDefault(inText) }.getOrDefault(inText)
BodyFormat.XML -> runCatching { formatXml(inText) }.getOrDefault(inText) BodyFormat.XML -> runCatching { formatXml(inText) }.getOrDefault(inText)
@@ -32,7 +32,7 @@ fun formatBody(inText: String, inFormat: BodyFormat): String =
// Minimal XML re-indenter: a newline between adjacent tags, one tab per // Minimal XML re-indenter: a newline between adjacent tags, one tab per
// nesting level (rendered at the editor's tab size). Declarations (<?…?>), comments / doctype (<!…>), self-closing tags and // nesting level (rendered at the editor's tab size). Declarations (<?…?>), comments / doctype (<!…>), self-closing tags and
// single-line <a>text</a> elements don't change depth. Best-effort — content // single-line <a>text</a> elements don't change depth. Best-effort - content
// with a literal '>' inside attributes or CDATA may not round-trip, hence the // with a literal '>' inside attributes or CDATA may not round-trip, hence the
// runCatching guard at the call site. // runCatching guard at the call site.
private fun formatXml(inText: String): String { private fun formatXml(inText: String): String {
+11 -11
View File
@@ -13,16 +13,16 @@ import okio.buffer
import kotlin.time.TimeSource import kotlin.time.TimeSource
// ================== // ==================
// MARK: HttpRunner — executes an ApiRequest through Ktor // MARK: HttpRunner - executes an ApiRequest through Ktor
// ================== // ==================
/** Holds one Ktor HttpClient for the app's lifetime. One engine on every desktop /** Holds one Ktor HttpClient for the app's lifetime. One engine on every desktop
target: Ktor's Curl engine (bundled libcurl — Schannel on Windows, OpenSSL on target: Ktor's Curl engine (bundled libcurl - Schannel on Windows, OpenSSL on
macOS/Linux). Same TLS stack as the client-cert path in CurlMtls.kt. macOS/Linux). Same TLS stack as the client-cert path in CurlMtls.kt.
run() is a suspend fun — call it off the UI dispatcher. */ run() is a suspend fun - call it off the UI dispatcher. */
class HttpRunner { class HttpRunner {
private val fClient = createApiHttpClient() private val mClient = createApiHttpClient()
init { init {
// Clear any temporary client certs a prior crash left in the Windows // Clear any temporary client certs a prior crash left in the Windows
@@ -32,11 +32,11 @@ class HttpRunner {
suspend fun run(inReq: ApiRequest): ApiResponse { suspend fun run(inReq: ApiRequest): ApiResponse {
// Client-certificate requests bypass Ktor (no engine exposes a cert API) // Client-certificate requests bypass Ktor (no engine exposes a cert API)
// and go straight through libcurl — same bundled TLS stack. // and go straight through libcurl - same bundled TLS stack.
if (inReq.hasClientCert) return curlSendWithClientCert(inReq) if (inReq.hasClientCert) return curlSendWithClientCert(inReq)
val vMark = TimeSource.Monotonic.markNow() val vMark = TimeSource.Monotonic.markNow()
return try { return try {
val vResp: HttpResponse = fClient.request(inReq.url.trim()) { val vResp: HttpResponse = mClient.request(inReq.url.trim()) {
method = HttpMethod.parse(inReq.method.name) method = HttpMethod.parse(inReq.method.name)
inReq.params inReq.params
.filter { it.enabled && it.key.isNotBlank() } .filter { it.enabled && it.key.isNotBlank() }
@@ -81,7 +81,7 @@ class HttpRunner {
.sortedBy { it.first.lowercase() }, .sortedBy { it.first.lowercase() },
body = when { body = when {
vIsImage -> "" vIsImage -> ""
vBinary -> "(${vContentType ?: "binary"} · ${vBody.size} bytes — not shown; use Save as…)" vBinary -> "(${vContentType ?: "binary"} · ${vBody.size} bytes - not shown; use Save as…)"
else -> vBody.decodeToString() else -> vBody.decodeToString()
}, },
bytes = vBody, bytes = vBody,
@@ -97,13 +97,13 @@ class HttpRunner {
httpVersion = vResp.version.toString(), httpVersion = vResp.version.toString(),
) )
} catch (e: CancellationException) { } catch (e: CancellationException) {
// The caller cancelled (Cancel button) — let it propagate so the // The caller cancelled (Cancel button) - let it propagate so the
// request is dropped rather than reported as a failed response. // request is dropped rather than reported as a failed response.
throw e throw e
} catch (e: Throwable) { } catch (e: Throwable) {
ApiResponse( ApiResponse(
status = 0, status = 0,
statusText = "—", statusText = "-",
timeMs = vMark.elapsedNow().inWholeMilliseconds, timeMs = vMark.elapsedNow().inWholeMilliseconds,
sizeBytes = 0, sizeBytes = 0,
headers = emptyList(), headers = emptyList(),
@@ -113,7 +113,7 @@ class HttpRunner {
} }
} }
fun close() = fClient.close() fun close() = mClient.close()
} }
/** Heuristic for a non-text body: textual content types (text, json, xml, html, /** Heuristic for a non-text body: textual content types (text, json, xml, html,
@@ -129,7 +129,7 @@ internal fun isBinaryBody(inContentType: String?, inBytes: ByteArray): Boolean {
return inBytes.take(1024).any { it.toInt() == 0 } // no type: sniff for NUL bytes return inBytes.take(1024).any { it.toInt() == 0 } // no type: sniff for NUL bytes
} }
/** True when the bytes are gzip — either the response says so or they carry the /** True when the bytes are gzip - either the response says so or they carry the
gzip magic number (1f 8b). */ gzip magic number (1f 8b). */
private fun isGzip(inEncoding: String?, inBytes: ByteArray): Boolean = private fun isGzip(inEncoding: String?, inBytes: ByteArray): Boolean =
inEncoding?.contains("gzip", ignoreCase = true) == true || inEncoding?.contains("gzip", ignoreCase = true) == true ||
@@ -22,7 +22,7 @@ import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
/** A request tab pairing an inherited (read-only, source-tagged) list with the /** A request tab pairing an inherited (read-only, source-tagged) list with the
request's own editable list: Override copies an inherited entry down, and own request's own editable list: Override copies an inherited entry down, and own
rows that shadow an inherited one get an OverrideMark. Used by Query / Var / rows that shadow an inherited one get an OverrideMark. Used by Query / Var /
Headers — the only differences are the key case-sensitivity and the labels. */ Headers - the only differences are the key case-sensitivity and the labels. */
@Composable @Composable
internal fun InheritedEditableTab( internal fun InheritedEditableTab(
inInherited: List<InheritedKv>, inInherited: List<InheritedKv>,
@@ -131,7 +131,7 @@ internal fun SourceTag(inLabel: String, inPath: String) {
} }
} }
/** The tiny marker shown on an own value that shadows an inherited one — hover for /** The tiny marker shown on an own value that shadows an inherited one - hover for
"Overrides <key> from <path>". */ "Overrides <key> from <path>". */
@Composable @Composable
internal fun OverrideMark(inKey: String, inPath: String) { internal fun OverrideMark(inKey: String, inPath: String) {
+18 -18
View File
@@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
// ================== // ==================
/** A "pack" is a savable collection of requests plus its environment variables /** A "pack" is a savable collection of requests plus its environment variables
— the export/import unit. Variables are referenced as {{name}} in any URL, - the export/import unit. Variables are referenced as {{name}} in any URL,
query value, header or body and substituted just before the request is sent query value, header or body and substituted just before the request is sent
(see resolveVars in Tools.kt). Packs may be empty; the rich starter set lives (see resolveVars in Tools.kt). Packs may be empty; the rich starter set lives
in defaultSession(). */ in defaultSession(). */
@@ -27,7 +27,7 @@ data class Pack(
) )
/** A client-certificate (mutual TLS) configuration. Lives on a request today and /** A client-certificate (mutual TLS) configuration. Lives on a request today and
— with the pack tree — on the session / packs too (inherited downward). Maps - with the pack tree - on the session / packs too (inherited downward). Maps
to libcurl's CURLOPT_SSLCERT / SSLCERTTYPE / SSLKEY / SSLKEYTYPE / KEYPASSWD. */ to libcurl's CURLOPT_SSLCERT / SSLCERTTYPE / SSLKEY / SSLKEYTYPE / KEYPASSWD. */
@Serializable @Serializable
data class CertConfig( data class CertConfig(
@@ -39,7 +39,7 @@ data class CertConfig(
) )
/** The starter SESSION loaded on first launch (and on demand from the session /** The starter SESSION loaded on first launch (and on demand from the session
menu) — a guided tour of every feature against httpbin.org: the inheritance menu) - a guided tour of every feature against httpbin.org: the inheritance
ladder (session → pack → sub-pack → request, for variables / query params / ladder (session → pack → sub-pack → request, for variables / query params /
headers / client cert, innermost wins), loose root requests, a nested sub-pack, headers / client cert, innermost wins), loose root requests, a nested sub-pack,
per-request overrides, a client-cert pack, and a linked-copy pack. Each /get or per-request overrides, a client-cert pack, and a linked-copy pack. Each /get or
@@ -47,7 +47,7 @@ per-request overrides, a client-cert pack, and a linked-copy pack. Each /get or
inherited result is visible in the response. Loaded as an unsaved session. */ inherited result is visible in the response. Loaded as an unsaved session. */
fun defaultSession(): Session = Session( fun defaultSession(): Session = Session(
activePack = 0, activePack = 0,
// ── Session level — the base of every inheritance ladder ── // ── Session level - the base of every inheritance ladder ──
globalEnv = listOf( globalEnv = listOf(
KeyVal("baseUrl", "https://httpbin.org"), KeyVal("baseUrl", "https://httpbin.org"),
KeyVal("token", "session-token-abc"), KeyVal("token", "session-token-abc"),
@@ -64,7 +64,7 @@ fun defaultSession(): Session = Session(
), // every request gets ?trace=session (echoed in /get args) ), // every request gets ?trace=session (echoed in /get args)
// No session-level cert: it would route every request through mTLS and fail // No session-level cert: it would route every request through mTLS and fail
// without a real cert file. Client certs are shown on the "Secure (mTLS)" pack. // without a real cert file. Client certs are shown on the "Secure (mTLS)" pack.
// ── Loose requests at the session root (in no pack — inherit session only) ── // ── Loose requests at the session root (in no pack - inherit session only) ──
root = Pack( root = Pack(
isRoot = true, name = "", requests = listOf( isRoot = true, name = "", requests = listOf(
ApiRequest(name = "Ping (loose)", method = ReqMethod.GET, url = "{{baseUrl}}/get"), ApiRequest(name = "Ping (loose)", method = ReqMethod.GET, url = "{{baseUrl}}/get"),
@@ -72,7 +72,7 @@ fun defaultSession(): Session = Session(
) )
), ),
packs = listOf( packs = listOf(
// ── Methods — pack header/param/var inherited by its requests, plus a // ── Methods - pack header/param/var inherited by its requests, plus a
// nested sub-pack and per-request overrides of each kind. ── // nested sub-pack and per-request overrides of each kind. ──
SavedPack( SavedPack(
pack = Pack( pack = Pack(
@@ -92,15 +92,15 @@ fun defaultSession(): Session = Session(
), // overrides the session's apiVer for this pack ), // overrides the session's apiVer for this pack
requests = listOf( requests = listOf(
ApiRequest( ApiRequest(
name = "GET — echoes inherited", method = ReqMethod.GET, url = "{{baseUrl}}/get", name = "GET - echoes inherited", method = ReqMethod.GET, url = "{{baseUrl}}/get",
params = listOf(KeyVal("q", "compose")) params = listOf(KeyVal("q", "compose"))
), // own q + inherited trace=session + source=methods ), // own q + inherited trace=session + source=methods
ApiRequest( ApiRequest(
name = "GET — overrides query param", method = ReqMethod.GET, url = "{{baseUrl}}/get", name = "GET - overrides query param", method = ReqMethod.GET, url = "{{baseUrl}}/get",
params = listOf(KeyVal("trace", "request")) params = listOf(KeyVal("trace", "request"))
), // request's trace wins over the session's ), // request's trace wins over the session's
ApiRequest( ApiRequest(
name = "POST — overrides header", name = "POST - overrides header",
method = ReqMethod.POST, method = ReqMethod.POST,
url = "{{baseUrl}}/post", url = "{{baseUrl}}/post",
headers = listOf(KeyVal("Accept", "text/plain")), // request's Accept wins over the pack's headers = listOf(KeyVal("Accept", "text/plain")), // request's Accept wins over the pack's
@@ -109,17 +109,17 @@ fun defaultSession(): Session = Session(
body = "{\n \"user\": \"{{user}}\",\n \"api\": \"{{apiVer}}\"\n}" body = "{\n \"user\": \"{{user}}\",\n \"api\": \"{{apiVer}}\"\n}"
), ),
ApiRequest( ApiRequest(
name = "POST — overrides {{user}} var", method = ReqMethod.POST, url = "{{baseUrl}}/anything", name = "POST - overrides {{user}} var", method = ReqMethod.POST, url = "{{baseUrl}}/anything",
variables = listOf( variables = listOf(
KeyVal( KeyVal(
"user", "user",
"request-user" "request-user"
) )
), // request var beats session/pack — see body echo ), // request var beats session/pack - see body echo
bodyType = BodyType.TEXT, bodyFormat = BodyFormat.JSON, body = "{ \"who\": \"{{user}}\" }" bodyType = BodyType.TEXT, bodyFormat = BodyFormat.JSON, body = "{ \"who\": \"{{user}}\" }"
), ),
ApiRequest( ApiRequest(
name = "PUT — replace", name = "PUT - replace",
method = ReqMethod.PUT, method = ReqMethod.PUT,
url = "{{baseUrl}}/put", url = "{{baseUrl}}/put",
bodyType = BodyType.TEXT, bodyType = BodyType.TEXT,
@@ -132,7 +132,7 @@ fun defaultSession(): Session = Session(
bodyType = BodyType.TEXT, bodyFormat = BodyFormat.JSON, body = "{ \"hello\": \"world\" }" bodyType = BodyType.TEXT, bodyFormat = BodyFormat.JSON, body = "{ \"hello\": \"world\" }"
), ),
), ),
// Nested sub-pack — inherits Session → Methods → here (apiVer becomes v3, // Nested sub-pack - inherits Session → Methods → here (apiVer becomes v3,
// plus its own X-Nested header; trace / source / Accept still flow down). // plus its own X-Nested header; trace / source / Accept still flow down).
subPacks = listOf( subPacks = listOf(
Pack( Pack(
@@ -141,7 +141,7 @@ fun defaultSession(): Session = Session(
headers = listOf(KeyVal("X-Nested", "true")), headers = listOf(KeyVal("X-Nested", "true")),
requests = listOf( requests = listOf(
ApiRequest( ApiRequest(
name = "GET — deep inheritance", name = "GET - deep inheritance",
method = ReqMethod.GET, method = ReqMethod.GET,
url = "{{baseUrl}}/anything/{{apiVer}}" url = "{{baseUrl}}/anything/{{apiVer}}"
), ),
@@ -150,7 +150,7 @@ fun defaultSession(): Session = Session(
) )
) )
), ),
// ── Auth & status — uses the inherited {{token}} / {{user}} / {{password}} ── // ── Auth & status - uses the inherited {{token}} / {{user}} / {{password}} ──
SavedPack( SavedPack(
pack = Pack( pack = Pack(
name = "Auth & status", color = 2, requests = listOf( name = "Auth & status", color = 2, requests = listOf(
@@ -170,7 +170,7 @@ fun defaultSession(): Session = Session(
) )
) )
), ),
// ── Formats — response viewer (JSON / XML / HTML / image) ── // ── Formats - response viewer (JSON / XML / HTML / image) ──
SavedPack( SavedPack(
pack = Pack( pack = Pack(
name = "Formats", color = 3, requests = listOf( name = "Formats", color = 3, requests = listOf(
@@ -183,7 +183,7 @@ fun defaultSession(): Session = Session(
) )
) )
), ),
// ── Secure (mTLS) — a PACK-LEVEL client cert, inherited by its requests. // ── Secure (mTLS) - a PACK-LEVEL client cert, inherited by its requests.
// Open a request's Cert tab to see the inherited cert (source pill + // Open a request's Cert tab to see the inherited cert (source pill +
// Override). Set real cert paths to actually send; the example paths // Override). Set real cert paths to actually send; the example paths
// won't load. The second request overrides with its own cert. ── // won't load. The second request overrides with its own cert. ──
@@ -303,7 +303,7 @@ fun ApiRequest.bodyContentType(): String? = when (bodyType) {
} }
// ================== // ==================
// MARK: Response (runtime only — not part of a pack) // MARK: Response (runtime only - not part of a pack)
// ================== // ==================
class ApiResponse( class ApiResponse(
+6 -6
View File
@@ -9,7 +9,7 @@ import okio.Path.Companion.toPath
// MARK: Pack persistence (okio + kotlinx.serialization) // MARK: Pack persistence (okio + kotlinx.serialization)
// ================== // ==================
private val fJson = Json { private val mJson = Json {
prettyPrint = true prettyPrint = true
prettyPrintIndent = " " prettyPrintIndent = " "
ignoreUnknownKeys = true ignoreUnknownKeys = true
@@ -20,7 +20,7 @@ private val fJson = Json {
error message. */ error message. */
fun exportPack(inPack: Pack, inPath: String): String? = try { fun exportPack(inPack: Pack, inPath: String): String? = try {
systemFileSystem.write(inPath.trim().toPath()) { systemFileSystem.write(inPath.trim().toPath()) {
writeUtf8(fJson.encodeToString(inPack)) writeUtf8(mJson.encodeToString(inPack))
} }
null null
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -30,7 +30,7 @@ fun exportPack(inPack: Pack, inPath: String): String? = try {
/** Read a pack back from inPath. */ /** Read a pack back from inPath. */
fun importPack(inPath: String): Result<Pack> = try { fun importPack(inPath: String): Result<Pack> = try {
val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() } val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
Result.success(fJson.decodeFromString<Pack>(vText)) Result.success(mJson.decodeFromString<Pack>(vText))
} catch (e: Throwable) { } catch (e: Throwable) {
Result.failure(e) Result.failure(e)
} }
@@ -38,7 +38,7 @@ fun importPack(inPath: String): Result<Pack> = try {
/** Write a whole self-contained session to inPath. Returns null on success. */ /** Write a whole self-contained session to inPath. Returns null on success. */
fun exportSession(inSession: Session, inPath: String): String? = try { fun exportSession(inSession: Session, inPath: String): String? = try {
systemFileSystem.write(inPath.trim().toPath()) { systemFileSystem.write(inPath.trim().toPath()) {
writeUtf8(fJson.encodeToString(inSession)) writeUtf8(mJson.encodeToString(inSession))
} }
null null
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -48,7 +48,7 @@ fun exportSession(inSession: Session, inPath: String): String? = try {
/** Read a session back from inPath. */ /** Read a session back from inPath. */
fun importSession(inPath: String): Result<Session> = try { fun importSession(inPath: String): Result<Session> = try {
val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() } val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
Result.success(fJson.decodeFromString<Session>(vText)) Result.success(mJson.decodeFromString<Session>(vText))
} catch (e: Throwable) { } catch (e: Throwable) {
Result.failure(e) Result.failure(e)
} }
@@ -85,7 +85,7 @@ fun writeBytesFile(inPath: String, inBytes: ByteArray): String? = try {
/** Re-indent a JSON response for display; returns the input unchanged if it /** Re-indent a JSON response for display; returns the input unchanged if it
isn't valid JSON (so plain-text / HTML responses still show). */ isn't valid JSON (so plain-text / HTML responses still show). */
fun prettyJsonOrRaw(inText: String): String = try { fun prettyJsonOrRaw(inText: String): String = try {
fJson.encodeToString(JsonElement.serializer(), fJson.parseToJsonElement(inText)) mJson.encodeToString(JsonElement.serializer(), mJson.parseToJsonElement(inText))
} catch (_: Throwable) { } catch (_: Throwable) {
inText inText
} }
+7 -7
View File
@@ -47,7 +47,7 @@ data class SavedPack(
) )
/** A whole session, the export/import unit for the working set: every open pack /** A whole session, the export/import unit for the working set: every open pack
(self-contained — full content embedded) plus the shared global env and which (self-contained - full content embedded) plus the shared global env and which
pack was active. Only one session is open at a time. */ pack was active. Only one session is open at a time. */
@Serializable @Serializable
data class Session( data class Session(
@@ -64,7 +64,7 @@ data class Session(
// MARK: Load / save (app-data dir via okio) // MARK: Load / save (app-data dir via okio)
// ================== // ==================
private val fStateJson = Json { private val mStateJson = Json {
ignoreUnknownKeys = true ignoreUnknownKeys = true
encodeDefaults = true encodeDefaults = true
prettyPrint = true prettyPrint = true
@@ -88,26 +88,26 @@ fun loadAppState(): AppState {
val vPath = stateFilePath() ?: return AppState() val vPath = stateFilePath() ?: return AppState()
return try { return try {
val vText = systemFileSystem.read(vPath.toPath()) { readUtf8() } val vText = systemFileSystem.read(vPath.toPath()) { readUtf8() }
fStateJson.decodeFromString<AppState>(vText) mStateJson.decodeFromString<AppState>(vText)
} catch (_: Throwable) { } catch (_: Throwable) {
AppState() AppState()
} }
} }
/** Write the app state. Best-effort — failures are swallowed (it's a cache of /** Write the app state. Best-effort - failures are swallowed (it's a cache of
the session, not the user's exported .json packs). */ the session, not the user's exported .json packs). */
fun saveAppState(inState: AppState) { fun saveAppState(inState: AppState) {
val vPath = stateFilePath() ?: return val vPath = stateFilePath() ?: return
try { try {
systemFileSystem.write(vPath.toPath()) { writeUtf8(fStateJson.encodeToString(inState)) } systemFileSystem.write(vPath.toPath()) { writeUtf8(mStateJson.encodeToString(inState)) }
} catch (_: Throwable) { } catch (_: Throwable) {
// ignore — nothing the user can act on, and packs can still be exported // ignore - nothing the user can act on, and packs can still be exported
} }
} }
/** Open the per-user app-data folder where the state file lives in the OS file /** Open the per-user app-data folder where the state file lives in the OS file
manager. Revealing the state file opens its containing folder, which SDL has manager. Revealing the state file opens its containing folder, which SDL has
already created even when nothing's been saved yet. Fire-and-forget — the already created even when nothing's been saved yet. Fire-and-forget - the
reveal launches off the main thread (see revealInFileManager). */ reveal launches off the main thread (see revealInFileManager). */
fun openSettingsFolder() { fun openSettingsFolder() {
val vPath = stateFilePath() ?: return val vPath = stateFilePath() ?: return
+6 -6
View File
@@ -49,7 +49,7 @@ internal fun OptionsMenu(
Text("Light mode", color = if (!inDark) c.accent else c.text, fontSize = 13.sp) Text("Light mode", color = if (!inDark) c.accent else c.text, fontSize = 13.sp)
}, onClick = { if (inDark) inOnToggleTheme(); vOpen = false }) }, onClick = { if (inDark) inOnToggleTheme(); vOpen = false })
HorizontalDivider(color = c.border) HorizontalDivider(color = c.border)
// Palette picker — a swatch of each theme's primary + a check on the active one. // Palette picker - a swatch of each theme's primary + a check on the active one.
Box(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 6.dp, bottom = 2.dp)) { Box(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 6.dp, bottom = 2.dp)) {
Text("Theme", color = c.dim, fontSize = 11.sp) Text("Theme", color = c.dim, fontSize = 11.sp)
} }
@@ -86,7 +86,7 @@ internal fun OptionsMenu(
// MARK: Method picker (dropdown) // MARK: Method picker (dropdown)
// ================== // ==================
/** Panel 2 — one unified bar: a method dropdown (coloured label + unfold arrows), /** Panel 2 - one unified bar: a method dropdown (coloured label + unfold arrows),
a borderless URL field that melts into the bar, and Send (or Cancel). */ a borderless URL field that melts into the bar, and Send (or Cancel). */
@Composable @Composable
internal fun UrlBar( internal fun UrlBar(
@@ -126,7 +126,7 @@ internal fun UrlBar(
} }
} }
// Borderless URL field — no box, so it reads as part of the bar. // Borderless URL field - no box, so it reads as part of the bar.
Box( Box(
modifier = Modifier.weight(1f).alpha(if (inReadOnly) 0.55f else 1f).onKeyEvent { ev -> modifier = Modifier.weight(1f).alpha(if (inReadOnly) 0.55f else 1f).onKeyEvent { ev ->
if (ev.type == KeyEventType.KeyDown && (ev.key == Key.Enter || ev.key == Key.NumPadEnter)) { if (ev.type == KeyEventType.KeyDown && (ev.key == Key.Enter || ev.key == Key.NumPadEnter)) {
@@ -154,8 +154,8 @@ internal fun UrlBar(
} }
// Inspect the server's TLS certificate chain (handshake-only probe). // Inspect the server's TLS certificate chain (handshake-only probe).
// During the probe the lock glyph swaps to a spinner IN PLACE — same // During the probe the lock glyph swaps to a spinner IN PLACE - same
// box, padding and size — so the bar doesn't shift. // box, padding and size - so the bar doesn't shift.
val vLockHoverSrc = remember { MutableInteractionSource() } val vLockHoverSrc = remember { MutableInteractionSource() }
val vLockHover by vLockHoverSrc.collectIsHoveredAsState() val vLockHover by vLockHoverSrc.collectIsHoveredAsState()
TooltipBox( TooltipBox(
@@ -185,7 +185,7 @@ internal fun UrlBar(
} }
// Send and Cancel are the same Material Button (same MinHeight + padding) // Send and Cancel are the same Material Button (same MinHeight + padding)
// so the bar never changes height when toggling — Cancel is just red. // so the bar never changes height when toggling - Cancel is just red.
if (inLoading) Button( if (inLoading) Button(
onClick = inOnCancel, onClick = inOnCancel,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
@@ -21,7 +21,7 @@ import com.compose.sdl.icons.MaterialSymbols
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
// ================== // ==================
// MARK: Panel 3 — request builder (Query / Headers / Body) // MARK: Panel 3 - request builder (Query / Headers / Body)
// ================== // ==================
/** Tabs to edit query params, headers and the body, a Preview toggle top-right, /** Tabs to edit query params, headers and the body, a Preview toggle top-right,
@@ -63,7 +63,7 @@ internal fun RequestBuilder(
} }
// Tabs, with the status message and a Preview toggle (icon only) on the // Tabs, with the status message and a Preview toggle (icon only) on the
// same line — the toggle no longer needs its own row. // same line - the toggle no longer needs its own row.
Row( Row(
modifier = Modifier.fillMaxWidth().padding(start = 12.dp, end = 10.dp, top = 6.dp), modifier = Modifier.fillMaxWidth().padding(start = 12.dp, end = 10.dp, top = 6.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -98,7 +98,7 @@ internal fun RequestBuilder(
} }
HorizontalDivider(color = c.border) HorizontalDivider(color = c.border)
// Tab content — scrolls, except a Text body which fills the whole panel // Tab content - scrolls, except a Text body which fills the whole panel
// (so it stays one editable surface you can click anywhere in). Greyed when // (so it stays one editable surface you can click anywhere in). Greyed when
// read-only (a linked-copy request). // read-only (a linked-copy request).
val vScroll = rememberScrollState() val vScroll = rememberScrollState()
@@ -117,7 +117,7 @@ internal fun RequestBuilder(
} }
} }
// Body-type selector — only on the Body tab, pinned at the bottom. For a // Body-type selector - only on the Body tab, pinned at the bottom. For a
// Text body the format/type picker sits beside it; that type drives both the // Text body the format/type picker sits beside it; that type drives both the
// syntax colours and the sent Content-Type. // syntax colours and the sent Content-Type.
if (inRs.reqTab == 3) { if (inRs.reqTab == 3) {
@@ -170,7 +170,7 @@ internal fun FormatButton(inOnClick: () -> Unit) {
HoverIconBtn(MaterialSymbols.AutoFixHigh, "Format (pretty-print)", inOnClick) HoverIconBtn(MaterialSymbols.AutoFixHigh, "Format (pretty-print)", inOnClick)
} }
/** Icon button with the toolbar's standard hover treatment — accent-tinted /** Icon button with the toolbar's standard hover treatment - accent-tinted
background + accent icon on hover (matching the TLS chain button). inActive background + accent icon on hover (matching the TLS chain button). inActive
keeps it lit, for toggles such as the preview eye. */ keeps it lit, for toggles such as the preview eye. */
@Composable @Composable
@@ -212,7 +212,7 @@ internal fun HoverIconBtn(
} }
/** Tab-size picker (2 / 4 / 8), shown left of the format button. Sets the global /** Tab-size picker (2 / 4 / 8), shown left of the format button. Sets the global
editor tab width via TextLayoutConfig — how wide a typed '\t' renders AND how editor tab width via TextLayoutConfig - how wide a typed '\t' renders AND how
deep the formatter indents. Snapshot-backed, so the change is live. */ deep the formatter indents. Snapshot-backed, so the change is live. */
@Composable @Composable
internal fun TabSizeSelector() { internal fun TabSizeSelector() {
@@ -258,7 +258,7 @@ internal fun TabSizeSelector() {
} }
} }
/** FILE body — pick a file; its path is stored in body and sent as raw bytes. */ /** FILE body - pick a file; its path is stored in body and sent as raw bytes. */
@Composable @Composable
internal fun FileBody(inReq: ApiRequest, inEdit: ((ApiRequest) -> ApiRequest) -> Unit) { internal fun FileBody(inReq: ApiRequest, inEdit: ((ApiRequest) -> ApiRequest) -> Unit) {
val c = LocalAppColors.current val c = LocalAppColors.current
@@ -271,7 +271,7 @@ internal fun FileBody(inReq: ApiRequest, inEdit: ((ApiRequest) -> ApiRequest) ->
} }
} }
/** Client-certificate (mTLS) editor — certificate + optional separate key + /** Client-certificate (mTLS) editor - certificate + optional separate key +
passphrase. PEM/DER are used directly by OpenSSL on macOS/Linux; on Windows passphrase. PEM/DER are used directly by OpenSSL on macOS/Linux; on Windows
they're imported into the certificate store for the request then removed they're imported into the certificate store for the request then removed
(see CurlMtls). PKCS#12 bundles its own private key. */ (see CurlMtls). PKCS#12 bundles its own private key. */
@@ -331,7 +331,7 @@ internal fun CertConfigEditor(
} }
} }
Text( Text(
inCert.keyPath.ifBlank { "Optional — only if the key is in a separate file." }, inCert.keyPath.ifBlank { "Optional - only if the key is in a separate file." },
color = if (inCert.keyPath.isNotBlank()) c.text else c.dim, fontSize = 12.sp, color = if (inCert.keyPath.isNotBlank()) c.text else c.dim, fontSize = 12.sp,
) )
} }
@@ -402,7 +402,7 @@ internal fun RequestCertTab(
} }
} }
/** Scope settings editor (Variables / Query / Headers / Cert sub-tabs) — shared by /** Scope settings editor (Variables / Query / Headers / Cert sub-tabs) - shared by
the session settings tab and each pack's settings tab. Each sub-tab shows what the session settings tab and each pack's settings tab. Each sub-tab shows what
the scope inherits from above (source-tagged, with Override) over its own the scope inherits from above (source-tagged, with Override) over its own
editable list, mirroring the request panels. */ editable list, mirroring the request panels. */
@@ -415,7 +415,7 @@ internal fun ScopeSettings(
inParams: List<KeyVal>, inOnParams: (List<KeyVal>) -> Unit, inParamHelp: String, inParams: List<KeyVal>, inOnParams: (List<KeyVal>) -> Unit, inParamHelp: String,
inHeaders: List<KeyVal>, inOnHeaders: (List<KeyVal>) -> Unit, inHeaderHelp: String, inHeaders: List<KeyVal>, inOnHeaders: (List<KeyVal>) -> Unit, inHeaderHelp: String,
inCert: CertConfig?, inOnCert: (CertConfig?) -> Unit, inCertHelp: String, inCertHeading: String, inCert: CertConfig?, inOnCert: (CertConfig?) -> Unit, inCertHelp: String, inCertHeading: String,
// What this scope inherits from above (empty for the session — it's the top). // What this scope inherits from above (empty for the session - it's the top).
inInheritedVars: List<InheritedKv> = emptyList(), inInheritedVars: List<InheritedKv> = emptyList(),
inInheritedParams: List<InheritedKv> = emptyList(), inInheritedParams: List<InheritedKv> = emptyList(),
inInheritedHeaders: List<InheritedKv> = emptyList(), inInheritedHeaders: List<InheritedKv> = emptyList(),
@@ -33,7 +33,7 @@ import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
/** Strip of open requests above the editor. Each tab is clickable to select /** Strip of open requests above the editor. Each tab is clickable to select
and drag-reorderable. While dragging, the grabbed tab lifts (zIndex), and drag-reorderable. While dragging, the grabbed tab lifts (zIndex),
fades, and follows the cursor via a graphicsLayer translation — its fades, and follows the cursor via a graphicsLayer translation - its
neighbours don't shuffle; the reorder commits on drop, to the slot whose neighbours don't shuffle; the reorder commits on drop, to the slot whose
neighbours' centres the cursor passed. key(tab) keeps each tab's neighbours' centres the cursor passed. key(tab) keeps each tab's
LayoutNode stable so the drag session survives the reorder. */ LayoutNode stable so the drag session survives the reorder. */
@@ -57,7 +57,7 @@ internal fun RequestTabStrip(
var vDragTarget by remember { mutableStateOf(-1) } var vDragTarget by remember { mutableStateOf(-1) }
// One shared context menu, anchored to the tab that opened it (m3 DropdownMenu // One shared context menu, anchored to the tab that opened it (m3 DropdownMenu
// is parent-anchored, not cursor-anchored — the old x/y capture is gone). // is parent-anchored, not cursor-anchored - the old x/y capture is gone).
var vMenu by remember { mutableStateOf(false) } var vMenu by remember { mutableStateOf(false) }
var vMenuTab by remember { mutableStateOf<StripTab?>(null) } var vMenuTab by remember { mutableStateOf<StripTab?>(null) }
@@ -89,7 +89,7 @@ internal fun RequestTabStrip(
.onGloballyPositioned { vLeft[vKey] = it.x } .onGloballyPositioned { vLeft[vKey] = it.x }
.onSizeChanged { vWidth[vKey] = it.width } .onSizeChanged { vWidth[vKey] = it.width }
// NOTE: `alpha` and `translationX` MUST be on the same graphicsLayer. // NOTE: `alpha` and `translationX` MUST be on the same graphicsLayer.
// `Modifier.alpha` expands to `graphicsLayer(alpha, clip = true)` — clip is // `Modifier.alpha` expands to `graphicsLayer(alpha, clip = true)` - clip is
// applied in the ALPHA layer's local coord space (before any child layer's // applied in the ALPHA layer's local coord space (before any child layer's
// translation), so `.alpha().graphicsLayer(translationX = X)` clips the drag // translation), so `.alpha().graphicsLayer(translationX = X)` clips the drag
// ghost inside its original bounds and the ghost disappears as it slides. One // ghost inside its original bounds and the ghost disappears as it slides. One
@@ -103,7 +103,7 @@ internal fun RequestTabStrip(
.pointerInput(vKey) { .pointerInput(vKey) {
// Accumulate the per-frame delta rather than reading change.position. // Accumulate the per-frame delta rather than reading change.position.
// The tab wears a graphicsLayer(translationX = vDragDx) while dragging, // The tab wears a graphicsLayer(translationX = vDragDx) while dragging,
// which transforms the modifier's local pointer frame — so position.x // which transforms the modifier's local pointer frame - so position.x
// would compensate for the translation each frame and the tab would // would compensate for the translation each frame and the tab would
// trail the mouse by roughly half its offset. dragAmount is the // trail the mouse by roughly half its offset. dragAmount is the
// root-frame delta and doesn't feed back through the layer transform. // root-frame delta and doesn't feed back through the layer transform.
@@ -143,7 +143,7 @@ internal fun RequestTabStrip(
) )
} }
// Middle-click closes the tab; right-click opens the tab menu. Compose // Middle-click closes the tab; right-click opens the tab menu. Compose
// has no first-class match for either — inline pointerInput checks the // has no first-class match for either - inline pointerInput checks the
// pressed PointerButton at each Press. // pressed PointerButton at each Press.
.pointerInput(vTab) { .pointerInput(vTab) {
awaitPointerEventScope { awaitPointerEventScope {
+11 -11
View File
@@ -79,7 +79,7 @@ internal fun PackColorPicker(inSelected: Int, inOnPick: (Int) -> Unit) {
// MARK: Add-pack menu (header '+') // MARK: Add-pack menu (header '+')
// ================== // ==================
/** The header '+' — a small menu to add a pack to the open session, either blank /** The header '+' - a small menu to add a pack to the open session, either blank
or imported from a .json file. Importing a pack always lands it in the session. */ or imported from a .json file. Importing a pack always lands it in the session. */
@Composable @Composable
internal fun AddPackMenu(inOnNewRequest: () -> Unit, inOnNew: () -> Unit, inOnImport: () -> Unit) { internal fun AddPackMenu(inOnNewRequest: () -> Unit, inOnNew: () -> Unit, inOnImport: () -> Unit) {
@@ -131,7 +131,7 @@ internal fun ellipsizeMiddle(inText: String, inMax: Int = 46): String {
} }
/** Header dropdown for the one open session. Shows its file name with the full /** Header dropdown for the one open session. Shows its file name with the full
path underneath when it has a file (so you can see it's saved — file-backed path underneath when it has a file (so you can see it's saved - file-backed
sessions auto-save), or "Untitled session" with a dot when it has no file yet. sessions auto-save), or "Untitled session" with a dot when it has no file yet.
Menu: Save / Save as… / Rename / Reveal / Open… / New + a recent list. */ Menu: Save / Save as… / Rename / Reveal / Open… / New + a recent list. */
@Composable @Composable
@@ -240,7 +240,7 @@ internal fun SessionMenu(
HorizontalDivider(color = c.border) HorizontalDivider(color = c.border)
// Aligned to the icon column (the DropdownMenuItem's 16dp pad), so // Aligned to the icon column (the DropdownMenuItem's 16dp pad), so
// it lines up with the recent items' file icon. Padding goes on a // it lines up with the recent items' file icon. Padding goes on a
// wrapping Box — a leaf Text's own start padding isn't applied to its // wrapping Box - a leaf Text's own start padding isn't applied to its
// draw position by this layout engine. // draw position by this layout engine.
Box(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 6.dp, bottom = 2.dp)) { Box(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 6.dp, bottom = 2.dp)) {
Text("Recent", color = c.dim, fontSize = 11.sp) Text("Recent", color = c.dim, fontSize = 11.sp)
@@ -263,7 +263,7 @@ internal fun SessionMenu(
Text(fileLeaf(vPath), color = c.text, fontSize = 12.sp) Text(fileLeaf(vPath), color = c.text, fontSize = 12.sp)
Text(ellipsizeMiddle(vPath, 32), color = c.dim, fontSize = 10.sp, softWrap = false) Text(ellipsizeMiddle(vPath, 32), color = c.dim, fontSize = 10.sp, softWrap = false)
} }
// Reveal this session's folder — its own hover + tooltip so it // Reveal this session's folder - its own hover + tooltip so it
// reads as a separate action from "open the session". // reads as a separate action from "open the session".
TooltipBox( TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider( positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
@@ -332,7 +332,7 @@ internal class PackOps(
/** Renders a pack then its sub-packs recursively, each indented by its depth. /** Renders a pack then its sub-packs recursively, each indented by its depth.
inSiblings is the list this pack belongs to (top-level vPacks, or a parent's inSiblings is the list this pack belongs to (top-level vPacks, or a parent's
subPacks) — used to position the pack-reorder drop bars by index. */ subPacks) - used to position the pack-reorder drop bars by index. */
@Composable @Composable
internal fun PackTree( internal fun PackTree(
inPack: PackState, inPack: PackState,
@@ -519,7 +519,7 @@ internal fun PackSection(
Text(inPack.name, color = c.text, fontSize = 14.sp, modifier = Modifier.weight(1f)) Text(inPack.name, color = c.text, fontSize = 14.sp, modifier = Modifier.weight(1f))
Text("${inPack.requests.size}", color = c.dim, fontSize = 11.sp) Text("${inPack.requests.size}", color = c.dim, fontSize = 11.sp)
} }
// + (new request) and ⋮ (pack menu) — both reveal on hover only, so the // + (new request) and ⋮ (pack menu) - both reveal on hover only, so the
// header width never shifts (alpha, not conditional layout). A linked // header width never shifts (alpha, not conditional layout). A linked
// copy mirrors its source's requests, so it has no "new request". // copy mirrors its source's requests, so it has no "new request".
Row( Row(
@@ -573,7 +573,7 @@ internal fun PackSection(
} }
// ============ // ============
// Pack body — request list (expanded, or always for the loose root). // Pack body - request list (expanded, or always for the loose root).
// Rows drag within and across packs via the shared controller; the drop // Rows drag within and across packs via the shared controller; the drop
// bar shows here whenever this pack is the resolved target. // bar shows here whenever this pack is the resolved target.
if (inPack.expanded || inHeaderless) { if (inPack.expanded || inHeaderless) {
@@ -584,7 +584,7 @@ internal fun PackSection(
val vBarBefore = if (vIsTarget) vOthers.getOrNull(inDrag.dropIndex) else null val vBarBefore = if (vIsTarget) vOthers.getOrNull(inDrag.dropIndex) else null
val vBarAtEnd = vIsTarget && inDrag.dropIndex >= vOthers.size val vBarAtEnd = vIsTarget && inDrag.dropIndex >= vOthers.size
// The loose root has no header, so register the body box as its drop // The loose root has no header, so register the body box as its drop
// anchor — that gives an empty root a region to target while dragging. // anchor - that gives an empty root a region to target while dragging.
var vBodyMod = Modifier.fillMaxWidth().padding(start = 10.dp, top = 2.dp, bottom = 4.dp) var vBodyMod = Modifier.fillMaxWidth().padding(start = 10.dp, top = 2.dp, bottom = 4.dp)
if (inHeaderless) vBodyMod = vBodyMod if (inHeaderless) vBodyMod = vBodyMod
.onGloballyPositioned { inDrag.headTop[inPack] = it.y } .onGloballyPositioned { inDrag.headTop[inPack] = it.y }
@@ -605,7 +605,7 @@ internal fun PackSection(
key(vRs) { key(vRs) {
val vDragged = vRs === inDrag.dragReq && vMoving val vDragged = vRs === inDrag.dragReq && vMoving
var vMod: Modifier = Modifier var vMod: Modifier = Modifier
// Linked copies mirror the source's requests — read-only, so // Linked copies mirror the source's requests - read-only, so
// their rows neither register geometry (would clobber the // their rows neither register geometry (would clobber the
// source's) nor drag. // source's) nor drag.
if (!inPack.isLinked) vMod = vMod if (!inPack.isLinked) vMod = vMod
@@ -616,7 +616,7 @@ internal fun PackSection(
if (vDragged) vMod = if (vDragged) vMod =
vMod.zIndex(1f).graphicsLayer(alpha = 0.65f, translationX = 0f, translationY = inDrag.dy) vMod.zIndex(1f).graphicsLayer(alpha = 0.65f, translationX = 0f, translationY = inDrag.dy)
if (!inPack.isLinked) vMod = vMod.pointerInput(vRs) { if (!inPack.isLinked) vMod = vMod.pointerInput(vRs) {
// Same graphicsLayer feedback concern as the pack drag — // Same graphicsLayer feedback concern as the pack drag -
// accumulate dragAmount instead of reading change.position. // accumulate dragAmount instead of reading change.position.
detectDragGestures( detectDragGestures(
onDragStart = { offset -> onDragStart = { offset ->
@@ -706,7 +706,7 @@ internal fun RequestRow(
) { ) {
MethodTag(vReq.method) MethodTag(vReq.method)
Text(vReq.name, color = c.text, fontSize = 13.sp, modifier = Modifier.weight(1f)) Text(vReq.name, color = c.text, fontSize = 13.sp, modifier = Modifier.weight(1f))
// Overflow (vertical-dots) menu — always laid out so the row width // Overflow (vertical-dots) menu - always laid out so the row width
// never changes; only its opacity toggles on hover / while open. Delete // never changes; only its opacity toggles on hover / while open. Delete
// lives inside this menu (no separate button on the row). // lives inside this menu (no separate button on the row).
Box(modifier = Modifier.alpha(if (vHover || vMenu) 1f else 0f)) { Box(modifier = Modifier.alpha(if (vHover || vMenu) 1f else 0f)) {
@@ -7,11 +7,11 @@ import androidx.compose.ui.text.buildAnnotatedString
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
// ================== // ==================
// MARK: BodyFormat — viewer/highlighter selection // MARK: BodyFormat - viewer/highlighter selection
// ================== // ==================
/** Format of a body payload as shown in the viewer, and (for a TEXT request body) /** Format of a body payload as shown in the viewer, and (for a TEXT request body)
the type the user picks on the right of the Body tab — it drives both syntax the type the user picks on the right of the Body tab - it drives both syntax
highlighting and the sent Content-Type. RAW disables highlighting and falls back highlighting and the sent Content-Type. RAW disables highlighting and falls back
to the selectable BasicTextField; other values pick a tokeniser. */ to the selectable BasicTextField; other values pick a tokeniser. */
@Serializable @Serializable
@@ -68,7 +68,7 @@ data class SyntaxPalette(
val punct: Color, val punct: Color,
) { ) {
companion object { companion object {
/** Light-on-dark — VS Code Dark+. */ /** Light-on-dark - VS Code Dark+. */
val Dark = SyntaxPalette( val Dark = SyntaxPalette(
key = Color(0xFF9CDCFE), key = Color(0xFF9CDCFE),
string = Color(0xFFCE9178), string = Color(0xFFCE9178),
@@ -79,7 +79,7 @@ data class SyntaxPalette(
punct = Color(0xFFCCCCCC), punct = Color(0xFFCCCCCC),
) )
/** Dark-on-light — VS Code Light+. Higher contrast on white. */ /** Dark-on-light - VS Code Light+. Higher contrast on white. */
val Light = SyntaxPalette( val Light = SyntaxPalette(
key = Color(0xFF0451A5), key = Color(0xFF0451A5),
string = Color(0xFFA31515), string = Color(0xFFA31515),
@@ -113,13 +113,13 @@ fun highlight(
} }
// ================== // ==================
// MARK: JSON tokeniser — single-pass, error-tolerant // MARK: JSON tokeniser - single-pass, error-tolerant
// ================== // ==================
/** Tokeniser for JSON. Walks the string once and emits style spans for /** Tokeniser for JSON. Walks the string once and emits style spans for
strings, numbers, booleans, null, punctuation. Distinguishes "keys" strings, numbers, booleans, null, punctuation. Distinguishes "keys"
(strings followed by ':') from string values by peeking after each (strings followed by ':') from string values by peeking after each
string. Error-tolerant — malformed JSON still highlights as best string. Error-tolerant - malformed JSON still highlights as best
effort rather than throwing. */ effort rather than throwing. */
private fun highlightJson(inText: String, inP: SyntaxPalette): AnnotatedString = buildAnnotatedString { private fun highlightJson(inText: String, inP: SyntaxPalette): AnnotatedString = buildAnnotatedString {
val vN = inText.length val vN = inText.length
@@ -224,7 +224,7 @@ private fun highlightXml(inText: String, inP: SyntaxPalette): AnnotatedString =
append(inText.substring(vTagStart, vJ)) append(inText.substring(vTagStart, vJ))
pop() pop()
} }
// Attributes — name="value" pairs. // Attributes - name="value" pairs.
while (vJ < vGt - 1) { while (vJ < vGt - 1) {
val vC2 = inText[vJ] val vC2 = inText[vJ]
when { when {
@@ -271,7 +271,7 @@ private fun highlightXml(inText: String, inP: SyntaxPalette): AnnotatedString =
} }
// ================== // ==================
// MARK: YAML tokeniser — line-based // MARK: YAML tokeniser - line-based
// ================== // ==================
/** Tokeniser for YAML. Walks line by line because YAML's grammar is /** Tokeniser for YAML. Walks line by line because YAML's grammar is
@@ -320,7 +320,7 @@ private fun highlightYaml(inText: String, inP: SyntaxPalette): AnnotatedString =
} }
} }
/** Render the value portion of a YAML line — applies string / number / /** Render the value portion of a YAML line - applies string / number /
keyword / list-marker colours where applicable. */ keyword / list-marker colours where applicable. */
private fun AnnotatedString.Builder.appendYamlValue(inSeg: String, inP: SyntaxPalette) { private fun AnnotatedString.Builder.appendYamlValue(inSeg: String, inP: SyntaxPalette) {
val vTrimmed = inSeg.trimStart() val vTrimmed = inSeg.trimStart()
@@ -378,7 +378,7 @@ private fun findUnquotedHash(inLine: String): Int {
} }
/** Locate the colon that separates a YAML key from its value at the /** Locate the colon that separates a YAML key from its value at the
current indent level — the FIRST unquoted ':' that's followed by current indent level - the FIRST unquoted ':' that's followed by
either whitespace or end-of-line. Returns -1 if the line has no key. */ either whitespace or end-of-line. Returns -1 if the line has no key. */
private fun indexOfKeyColon(inLine: String): Int { private fun indexOfKeyColon(inLine: String): Int {
var vInSingle = false var vInSingle = false
+1 -1
View File
@@ -4,7 +4,7 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
// ================== // ==================
// MARK: Theme — the app's legacy colour slots, derived from the active palette // MARK: Theme - the app's legacy colour slots, derived from the active palette
// ================== // ==================
// AppColors is the app's own colour vocabulary (used by the custom widgets). It // AppColors is the app's own colour vocabulary (used by the custom widgets). It
// is now DERIVED from the selected Voltic M3 ColorScheme via appColorsFromScheme // is now DERIVED from the selected Voltic M3 ColorScheme via appColorsFromScheme
@@ -125,7 +125,7 @@ internal fun CertCard(inIndex: Int, inCert: ChainCert) {
vSubject?.let { CertLine("Subject", it) } vSubject?.let { CertLine("Subject", it) }
if (vSelfSigned) CertLine("Issuer", "Self-signed", kSelfSignedColor) if (vSelfSigned) CertLine("Issuer", "Self-signed", kSelfSignedColor)
else vIssuer?.let { CertLine("Issuer", it) } else vIssuer?.let { CertLine("Issuer", it) }
// Hosts this cert is valid for — the field that actually has to match the URL. // Hosts this cert is valid for - the field that actually has to match the URL.
(certField(vFields, "X509v3 Subject Alternative Name") (certField(vFields, "X509v3 Subject Alternative Name")
?: certField(vFields, "Subject Alternative Name") ?: certField(vFields, "Subject Alternative Name")
?: vParsed?.sans?.takeIf { it.isNotEmpty() }?.joinToString(", ")) ?: vParsed?.sans?.takeIf { it.isNotEmpty() }?.joinToString(", "))
@@ -141,7 +141,7 @@ internal fun CertCard(inIndex: Int, inCert: ChainCert) {
internal val kSelfSignedColor = Color(0xFF3FB950L) internal val kSelfSignedColor = Color(0xFF3FB950L)
/** Copy button — rounded, hover overlay, real click that copies inText and shows /** Copy button - rounded, hover overlay, real click that copies inText and shows
a green check. Icon-only (per cert) flashes a small floating "Copied" bubble a green check. Icon-only (per cert) flashes a small floating "Copied" bubble
for 2s; the labelled variant (Copy chain) flips its label to "Copied" instead. for 2s; the labelled variant (Copy chain) flips its label to "Copied" instead.
Uses a non-catching Popup so it never dismisses the dialog or eats clicks. */ Uses a non-catching Popup so it never dismisses the dialog or eats clicks. */
@@ -218,7 +218,7 @@ format the platform backends emit:
"CN=R3, O=Let's Encrypt, C=US" (Windows CertGetNameStringA) "CN=R3, O=Let's Encrypt, C=US" (Windows CertGetNameStringA)
"/C=US/O=Let's Encrypt/CN=R3" (OpenSSL X509_NAME_oneline) "/C=US/O=Let's Encrypt/CN=R3" (OpenSSL X509_NAME_oneline)
"CN = R3, O = Let's Encrypt, C = US" (OpenSSL X509_NAME_print_ex "CN = R3, O = Let's Encrypt, C = US" (OpenSSL X509_NAME_print_ex
default, with spaces — what default, with spaces - what
libcurl on macOS emits) libcurl on macOS emits)
Null when there's no CN. */ Null when there's no CN. */
internal fun cnOf(inDn: String): String? { internal fun cnOf(inDn: String): String? {
+3 -3
View File
@@ -4,14 +4,14 @@ package apidemo
// MARK: TLS chain / client-certificate seam // MARK: TLS chain / client-certificate seam
// ================== // ==================
// The real implementations drive the bundled libcurl directly (CurlMtls.kt, // The real implementations drive the bundled libcurl directly (CurlMtls.kt,
// nativeMain) — Ktor's engines expose no client-cert / CERTINFO API. The jvm // nativeMain) - Ktor's engines expose no client-cert / CERTINFO API. The jvm
// parity app has no bundled libcurl, so its actuals report the feature as // parity app has no bundled libcurl, so its actuals report the feature as
// native-only instead. // native-only instead.
/** One certificate in the chain: its CURLINFO_CERTINFO-style fields (Subject / /** One certificate in the chain: its CURLINFO_CERTINFO-style fields (Subject /
Issuer / the PEM under "Cert" / dates …) and whether the server actually Issuer / the PEM under "Cert" / dates …) and whether the server actually
presented it. Derived certs — an issuer resolved from the OS store, or a presented it. Derived certs - an issuer resolved from the OS store, or a
name-only placeholder — have fromServer = false and are drawn dotted. */ name-only placeholder - have fromServer = false and are drawn dotted. */
class ChainCert(val fields: List<Pair<String, String>>, val fromServer: Boolean) class ChainCert(val fields: List<Pair<String, String>>, val fromServer: Boolean)
/** The server's TLS certificate chain. error is set instead when it couldn't be /** The server's TLS certificate chain. error is set instead when it couldn't be
+3 -3
View File
@@ -20,8 +20,8 @@ fun substituteVars(inText: String, inVars: List<KeyVal>): String {
return kVarRegex.replace(inText) { vMatch -> vMap[vMatch.groupValues[1]] ?: vMatch.value } return kVarRegex.replace(inText) { vMatch -> vMap[vMatch.groupValues[1]] ?: vMatch.value }
} }
/** Apply variable substitution across every field that actually gets sent — URL, /** Apply variable substitution across every field that actually gets sent - URL,
query params, headers and body — returning a fully-resolved request. The query params, headers and body - returning a fully-resolved request. The
original (template) request is left untouched. */ original (template) request is left untouched. */
fun resolveVars(inReq: ApiRequest, inVars: List<KeyVal>): ApiRequest { fun resolveVars(inReq: ApiRequest, inVars: List<KeyVal>): ApiRequest {
val vActive = inVars.any { it.enabled && it.key.isNotBlank() } val vActive = inVars.any { it.enabled && it.key.isNotBlank() }
@@ -54,7 +54,7 @@ fun resolveVars(inReq: ApiRequest, inVars: List<KeyVal>): ApiRequest {
} }
/** The distinct {{names}} referenced anywhere in the request that have no enabled /** The distinct {{names}} referenced anywhere in the request that have no enabled
variable to fill them — surfaced as a warning in the editor. */ variable to fill them - surfaced as a warning in the editor. */
fun unresolvedVars(inReq: ApiRequest, inVars: List<KeyVal>): List<String> { fun unresolvedVars(inReq: ApiRequest, inVars: List<KeyVal>): List<String> {
val vDefined = inVars val vDefined = inVars
.filter { it.enabled && it.key.isNotBlank() } .filter { it.enabled && it.key.isNotBlank() }
+4 -4
View File
@@ -35,7 +35,7 @@ width so request names line up. */
@Composable @Composable
internal fun MethodTag(inMethod: ReqMethod) { internal fun MethodTag(inMethod: ReqMethod) {
val vCol = methodColor(inMethod) val vCol = methodColor(inMethod)
// No background — just the coloured method name, fixed width so the request // No background - just the coloured method name, fixed width so the request
// names still line up in the sidebar list. // names still line up in the sidebar list.
Box( Box(
modifier = Modifier.width(46.dp).padding(vertical = 2.dp), modifier = Modifier.width(46.dp).padding(vertical = 2.dp),
@@ -53,7 +53,7 @@ internal fun RowDropBar() {
Box(modifier = Modifier.fillMaxWidth().height(2.dp).background(c.accent, RoundedCornerShape(1.dp))) Box(modifier = Modifier.fillMaxWidth().height(2.dp).background(c.accent, RoundedCornerShape(1.dp)))
} }
/** Tab indices listed in inDots get a small accent dot after their label — used /** Tab indices listed in inDots get a small accent dot after their label - used
to flag a tab that holds content (e.g. a non-empty request body). */ to flag a tab that holds content (e.g. a non-empty request body). */
@Composable @Composable
internal fun TabBar(inTabs: List<String>, inSelected: Int, inDots: Set<Int> = emptySet(), inOnSelect: (Int) -> Unit) { internal fun TabBar(inTabs: List<String>, inSelected: Int, inDots: Set<Int> = emptySet(), inOnSelect: (Int) -> Unit) {
@@ -90,7 +90,7 @@ internal fun TogglePill(inLabel: String, inSelected: Boolean, inOnClick: () -> U
) { Text(inLabel, color = if (inSelected) c.onAccent else c.dim, fontSize = 12.sp) } ) { Text(inLabel, color = if (inSelected) c.onAccent else c.dim, fontSize = 12.sp) }
} }
/** Compact single-line (or fixed-height multi-line) input — a BasicTextField in /** Compact single-line (or fixed-height multi-line) input - a BasicTextField in
a slim bordered box, much shorter than the 56 dp Material OutlinedTextField. */ a slim bordered box, much shorter than the 56 dp Material OutlinedTextField. */
@Composable @Composable
internal fun ThinField( internal fun ThinField(
@@ -177,7 +177,7 @@ internal fun OutlinedAction(inIcon: Int, inLabel: String, inOnClick: () -> Unit)
/** Filled red icon+label button for destructive / stop actions (delete, cancel, quit). /** Filled red icon+label button for destructive / stop actions (delete, cancel, quit).
Uses the m3 Button so its size + ripple + interaction match every other Uses the m3 Button so its size + ripple + interaction match every other
Button in the app — previously handrolled Box + background + clickable + padding Button in the app - previously handrolled Box + background + clickable + padding
gave it a different height / horizontal padding, and the Discard button sat gave it a different height / horizontal padding, and the Discard button sat
next to a Button ("Save first…") looking noticeably smaller in dialogs. */ next to a Button ("Save first…") looking noticeably smaller in dialogs. */
@Composable @Composable
+53 -53
View File
@@ -47,7 +47,7 @@ import kotlinx.coroutines.launch
// MARK: Response // MARK: Response
// ================== // ==================
/** Panel 4 — Request / Response viewer. Each tab stacks a HEADERS section over a /** Panel 4 - Request / Response viewer. Each tab stacks a HEADERS section over a
BODY section. "Request" shows the resolved request that would be sent (the BODY section. "Request" shows the resolved request that would be sent (the
Preview); "Response" shows the result (image-aware). A copy / save toolbar Preview); "Response" shows the result (image-aware). A copy / save toolbar
acts on whichever view is showing. */ acts on whichever view is showing. */
@@ -61,7 +61,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
val vPreview = inRs.preview val vPreview = inRs.preview
val vShowRequest = vPreview || inRs.viewTab == 0 val vShowRequest = vPreview || inRs.viewTab == 0
// Pretty-print once per received body (not every recomposition) — the whole // Pretty-print once per received body (not every recomposition) - the whole
// body is shown; the renderer line-culls + wrap-caches so even a 20k-line // body is shown; the renderer line-culls + wrap-caches so even a 20k-line
// payload stays cheap. (Previously capped at 20000 chars, which silently cut // payload stays cheap. (Previously capped at 20000 chars, which silently cut
// long responses off around line ~1100.) // long responses off around line ~1100.)
@@ -89,7 +89,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
Text("not sent", color = VolticTheme.extended.warning, fontSize = 11.sp) Text("not sent", color = VolticTheme.extended.warning, fontSize = 11.sp)
} else { } else {
// Request tab — adds the coloured method only once the // Request tab - adds the coloured method only once the
// request has actually been sent (sentReq != null). // request has actually been sent (sentReq != null).
val vSent = inRs.sentReq val vSent = inRs.sentReq
ViewerTab( ViewerTab(
@@ -99,7 +99,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
inSelected = inRs.viewTab == 0, inSelected = inRs.viewTab == 0,
inOnClick = { inRs.viewTab = 0 }, inOnClick = { inRs.viewTab = 0 },
) )
// Response tab — adds the coloured status only when a // Response tab - adds the coloured status only when a
// response (or error) is in. No placeholder during the // response (or error) is in. No placeholder during the
// pre-send / loading state. // pre-send / loading state.
val (vRespAccent, vRespColor) = when { val (vRespAccent, vRespColor) = when {
@@ -115,7 +115,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
inOnClick = { inRs.viewTab = 1 }, inOnClick = { inRs.viewTab = 1 },
) )
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
// Spinner only — the Cancel control already lives next to // Spinner only - the Cancel control already lives next to
// Send in the URL bar; no need for a duplicate here. // Send in the URL bar; no need for a duplicate here.
if (inRs.viewTab == 1 && vLoading) { if (inRs.viewTab == 1 && vLoading) {
CircularProgressIndicator(modifier = Modifier.size(14.dp), color = c.accent, strokeWidth = 2.dp) CircularProgressIndicator(modifier = Modifier.size(14.dp), color = c.accent, strokeWidth = 2.dp)
@@ -134,7 +134,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
val vSentHeaders = val vSentHeaders =
if (!vPreview) inRs.response?.requestHeaders?.takeIf { it.isNotEmpty() } else null if (!vPreview) inRs.response?.requestHeaders?.takeIf { it.isNotEmpty() } else null
// Synthesize wire-level headers (Host, Content-Length, User-Agent) // Synthesize wire-level headers (Host, Content-Length, User-Agent)
// the engine adds without telling Ktor — same set httpie shows. // the engine adds without telling Ktor - same set httpie shows.
val vRawHeaders = val vRawHeaders =
if (vSentHeaders != null) vSentHeaders else parseHeaderLines(requestHeadersText(vR)) if (vSentHeaders != null) vSentHeaders else parseHeaderLines(requestHeadersText(vR))
val vHeaders = synthesizeRequestHeaders(vR, vRawHeaders) val vHeaders = synthesizeRequestHeaders(vR, vRawHeaders)
@@ -204,7 +204,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
inAutoLabel = if (inRs.respFormatOverride == null) vAuto.label else null, inAutoLabel = if (inRs.respFormatOverride == null) vAuto.label else null,
) )
} }
// Wrap toggle — soft-wrap long lines (default) vs. no wrap + // Wrap toggle - soft-wrap long lines (default) vs. no wrap +
// horizontal scroll (one source line per row). Shown for any text // horizontal scroll (one source line per row). Shown for any text
// body (request preview or response), hidden for an image. // body (request preview or response), hidden for an image.
val vHasTextBody = (vRespHasContent && !vRespImage) || vReqHasContent val vHasTextBody = (vRespHasContent && !vRespImage) || vReqHasContent
@@ -257,7 +257,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
} }
// ================== // ==================
// MARK: HttpFlowView — flat httpie-style request/response layout // MARK: HttpFlowView - flat httpie-style request/response layout
// ================== // ==================
/** Renders the body of a Request or Response tab in a httpie-flavoured /** Renders the body of a Request or Response tab in a httpie-flavoured
@@ -280,12 +280,12 @@ internal fun HttpFlowView(
) { ) {
val c = LocalAppColors.current val c = LocalAppColors.current
val vState = rememberLazyListState() val vState = rememberLazyListState()
// Editor-style selection state — anchor + caret in absolute character offsets // Editor-style selection state - anchor + caret in absolute character offsets
// into inBody. Resets when the body changes so a stale range never highlights // into inBody. Resets when the body changes so a stale range never highlights
// a shorter/different response. Lives outside any SelectionContainer so it // a shorter/different response. Lives outside any SelectionContainer so it
// survives scroll and spans off-screen chunks (see the BodySel comment). // survives scroll and spans off-screen chunks (see the BodySel comment).
var vSel: BodySel? by remember(inBody) { mutableStateOf(null) } var vSel: BodySel? by remember(inBody) { mutableStateOf(null) }
// Selection background from the app theme — same colour that // Selection background from the app theme - same colour that
// LocalTextSelectionColors publishes to Compose text selection elsewhere. // LocalTextSelectionColors publishes to Compose text selection elsewhere.
val vSelColor = LocalTextSelectionColors.current.backgroundColor val vSelColor = LocalTextSelectionColors.current.backgroundColor
val vFocus = remember { FocusRequester() } val vFocus = remember { FocusRequester() }
@@ -297,12 +297,12 @@ internal fun HttpFlowView(
// auto-scroll effect that scrolls the LazyColumn while the pointer sits in // auto-scroll effect that scrolls the LazyColumn while the pointer sits in
// the top / bottom edge zone. // the top / bottom edge zone.
var vDragPos: Offset? by remember { mutableStateOf(null) } var vDragPos: Offset? by remember { mutableStateOf(null) }
// The body is highlighted ONCE, then sliced into BLOCKS of lines — one BasicText // The body is highlighted ONCE, then sliced into BLOCKS of lines - one BasicText
// per block, NOT one per line. Far fewer nodes / selectables / paragraph set-ups // per block, NOT one per line. Far fewer nodes / selectables / paragraph set-ups
// churn through the viewport while scrolling (the smoothness win), and the // churn through the viewport while scrolling (the smoothness win), and the
// O(total-spans) AnnotatedString.subSequence runs once per block at build time // O(total-spans) AnnotatedString.subSequence runs once per block at build time
// (memoised) instead of per visible line every frame. LazyColumn still virtualizes, // (memoised) instead of per visible line every frame. LazyColumn still virtualizes,
// so only the ~2-3 on-screen blocks are composed/measured — 12k lines stay cheap. // so only the ~2-3 on-screen blocks are composed/measured - 12k lines stay cheap.
val vDark = isDarkBg(c.bg) val vDark = isDarkBg(c.bg)
val vChunks = remember(inBody, inBodyFormat, vDark) { val vChunks = remember(inBody, inBodyFormat, vDark) {
if (inBody == null) emptyList() if (inBody == null) emptyList()
@@ -315,16 +315,16 @@ internal fun HttpFlowView(
// Per-visible-chunk TextLayoutResult, keyed by chunk index. Populated as each // Per-visible-chunk TextLayoutResult, keyed by chunk index. Populated as each
// BodyChunkRow's BasicText lays out, dropped implicitly when the chunks list // BodyChunkRow's BasicText lays out, dropped implicitly when the chunks list
// changes (new response) via the remember key. Off-screen chunks aren't in // changes (new response) via the remember key. Off-screen chunks aren't in
// this map — we can't hit-test them via mouse either since they're not painted. // this map - we can't hit-test them via mouse either since they're not painted.
val vChunkLayouts = remember(vChunks) { mutableStateMapOf<Int, TextLayoutResult>() } val vChunkLayouts = remember(vChunks) { mutableStateMapOf<Int, TextLayoutResult>() }
val vTotalLines = remember(inBody) { if (inBody == null) 0 else inBody.count { it == '\n' } + 1 } val vTotalLines = remember(inBody) { if (inBody == null) 0 else inBody.count { it == '\n' } + 1 }
// No-wrap mode pans horizontally; every block shares this one scroll state so the // No-wrap mode pans horizontally; every block shares this one scroll state so the
// whole body pans together (the gutter stays pinned — not scrolled). // whole body pans together (the gutter stays pinned - not scrolled).
val vHScroll = rememberScrollState() val vHScroll = rememberScrollState()
val vGutterWidth = (vTotalLines.coerceAtLeast(1).toString().length * 7 + 4).dp val vGutterWidth = (vTotalLines.coerceAtLeast(1).toString().length * 7 + 4).dp
// Per-line styles hoisted once (not allocated per line per frame). The rows use // Per-line styles hoisted once (not allocated per line per frame). The rows use
// BasicText with these fixed styles instead of material3 Text — no per-node // BasicText with these fixed styles instead of material3 Text - no per-node
// LocalTextStyle merge / LocalContentColor read — which is the main scroll- // LocalTextStyle merge / LocalContentColor read - which is the main scroll-
// smoothness win when thousands of line items churn through the viewport. // smoothness win when thousands of line items churn through the viewport.
val vBodyStyle = remember(c.text) { TextStyle(color = c.text, fontSize = 12.sp, fontFamily = monoFontFamily) } val vBodyStyle = remember(c.text) { TextStyle(color = c.text, fontSize = 12.sp, fontFamily = monoFontFamily) }
val vNumStyle = remember(c.dim) { val vNumStyle = remember(c.dim) {
@@ -339,7 +339,7 @@ internal fun HttpFlowView(
// Body-relative x offset (px) of the block's BasicText inside the outer Box. // Body-relative x offset (px) of the block's BasicText inside the outer Box.
// Mirrors BodyChunkRow's layout: 4dp Row start padding + gutter width + 6dp spacer. // Mirrors BodyChunkRow's layout: 4dp Row start padding + gutter width + 6dp spacer.
val vTextStartPx = with(density) { (4.dp + vGutterWidth + 6.dp).toPx() } val vTextStartPx = with(density) { (4.dp + vGutterWidth + 6.dp).toPx() }
// LazyColumn item index of chunk index 0 — status row + optional header // LazyColumn item index of chunk index 0 - status row + optional header
// table + divider. Used to scroll a specific chunk into view when the caret // table + divider. Used to scroll a specific chunk into view when the caret
// moves off-screen via keyboard. Only stable while we're on the "have body" // moves off-screen via keyboard. Only stable while we're on the "have body"
// branch, but that's the only branch that renders chunks in the first place. // branch, but that's the only branch that renders chunks in the first place.
@@ -358,7 +358,7 @@ internal fun HttpFlowView(
if (!vVisible) vScope.launch { vState.scrollToItem(vChunkItemOffset + vCi) } if (!vVisible) vScope.launch { vState.scrollToItem(vChunkItemOffset + vCi) }
} }
// Hit-test — turns a pointer position (outer Box local frame) into a global // Hit-test - turns a pointer position (outer Box local frame) into a global
// character offset into inBody, or null if the pointer isn't over any chunk. // character offset into inBody, or null if the pointer isn't over any chunk.
// Clamps past-viewport pointers to the nearest visible chunk boundary so a // Clamps past-viewport pointers to the nearest visible chunk boundary so a
// drag that overshoots still keeps extending toward that end. // drag that overshoots still keeps extending toward that end.
@@ -387,7 +387,7 @@ internal fun HttpFlowView(
// Drag auto-scroll: while a drag is in progress (vDragPos != null), tick // Drag auto-scroll: while a drag is in progress (vDragPos != null), tick
// every frame and scroll the LazyColumn when the pointer sits in a 40dp // every frame and scroll the LazyColumn when the pointer sits in a 40dp
// edge zone at the top / bottom. Speed ramps linearly with depth into the // edge zone at the top / bottom. Speed ramps linearly with depth into the
// zone up to 24dp per frame — roughly a page per second at 60fps at max // zone up to 24dp per frame - roughly a page per second at 60fps at max
// depth. After each scroll we re-hit-test the current pointer position so // depth. After each scroll we re-hit-test the current pointer position so
// the selection keeps extending onto the newly revealed chunks. // the selection keeps extending onto the newly revealed chunks.
val vEdgePx = with(density) { 40.dp.toPx() } val vEdgePx = with(density) { 40.dp.toPx() }
@@ -412,7 +412,7 @@ internal fun HttpFlowView(
// contentAlignment pins the narrow scrollbar to the right edge; the // contentAlignment pins the narrow scrollbar to the right edge; the
// fillMaxSize content fills the rest (this project's Box has no BoxScope, so // fillMaxSize content fills the rest (this project's Box has no BoxScope, so
// there's no Modifier.align — alignment is via contentAlignment). // there's no Modifier.align - alignment is via contentAlignment).
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -421,7 +421,7 @@ internal fun HttpFlowView(
// Mouse: press starts a selection, drag extends it, Shift+press extends // Mouse: press starts a selection, drag extends it, Shift+press extends
// from the existing anchor. We drive this by hand instead of going // from the existing anchor. We drive this by hand instead of going
// through SelectionContainer because SC only sees composed selectables // through SelectionContainer because SC only sees composed selectables
// (visible chunks) — Ctrl+A and Shift+Arrow can't reach off-screen text // (visible chunks) - Ctrl+A and Shift+Arrow can't reach off-screen text
// through it. Header items above the chunks stay non-selectable, which // through it. Header items above the chunks stay non-selectable, which
// matches the previous DisableSelection wrappers. // matches the previous DisableSelection wrappers.
.pointerInput(vChunks) { .pointerInput(vChunks) {
@@ -437,7 +437,7 @@ internal fun HttpFlowView(
val vDown = awaitPointerEvent() val vDown = awaitPointerEvent()
if (vDown.type != PointerEventType.Press) continue if (vDown.type != PointerEventType.Press) continue
val vChange = vDown.changes.firstOrNull() ?: continue val vChange = vDown.changes.firstOrNull() ?: continue
// Skip presses a child already handled — most importantly the // Skip presses a child already handled - most importantly the
// VerticalScrollbar sibling in this Box. Without this, dragging // VerticalScrollbar sibling in this Box. Without this, dragging
// the scrollbar would also start (and continuously extend) a // the scrollbar would also start (and continuously extend) a
// body selection because my hit-test still finds a chunk under // body selection because my hit-test still finds a chunk under
@@ -584,7 +584,7 @@ internal fun HttpFlowView(
if (inShowSecureLock) { if (inShowSecureLock) {
MaterialSymbolsOutlined( MaterialSymbolsOutlined(
icon = MaterialSymbols.Lock, icon = MaterialSymbols.Lock,
tint = Color(0xFF36B37E), // green — TLS verified by OS tint = Color(0xFF36B37E), // green - TLS verified by OS
size = 14.dp, size = 14.dp,
) )
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
@@ -592,7 +592,7 @@ internal fun HttpFlowView(
Text(inStatusLine, color = c.text, fontSize = 13.sp) Text(inStatusLine, color = c.text, fontSize = 13.sp)
} }
} }
// Headers as a key/value table — only when not collapsed. // Headers as a key/value table - only when not collapsed.
if (!inHeadersCollapsed && inHeaders.isNotEmpty()) { if (!inHeadersCollapsed && inHeaders.isNotEmpty()) {
item { item {
HeaderTable(inHeaders, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)) HeaderTable(inHeaders, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp))
@@ -611,7 +611,7 @@ internal fun HttpFlowView(
} }
} }
} else if (vChunks.isNotEmpty()) { } else if (vChunks.isNotEmpty()) {
// One BasicText per BLOCK of lines (not per line) — far fewer // One BasicText per BLOCK of lines (not per line) - far fewer
// nodes to churn while scrolling. Chunk key lets the pointer // nodes to churn while scrolling. Chunk key lets the pointer
// hit-test map a visible item back to its chunk index. // hit-test map a visible item back to its chunk index.
items(vChunks.size, key = { "chunk_$it" }, contentType = { "chunk" }) { vCi -> items(vChunks.size, key = { "chunk_$it" }, contentType = { "chunk" }) { vCi ->
@@ -658,7 +658,7 @@ where they've moved to. When they differ, the selected range is [ min..max ].
When equal, it's a zero-width caret still useful as a starting point for When equal, it's a zero-width caret still useful as a starting point for
subsequent Shift+Arrow / Shift+Click extensions. subsequent Shift+Arrow / Shift+Click extensions.
Note this lives OUTSIDE the SelectionContainer machinery — that machinery only Note this lives OUTSIDE the SelectionContainer machinery - that machinery only
tracks selectables composed by the LazyColumn (visible chunks), which is why tracks selectables composed by the LazyColumn (visible chunks), which is why
Ctrl+A and drag-select cannot reach off-screen text through it. This state is Ctrl+A and drag-select cannot reach off-screen text through it. This state is
plain integer offsets so it survives scroll and covers the whole body. */ plain integer offsets so it survives scroll and covers the whole body. */
@@ -676,7 +676,7 @@ private fun lineStartAt(inText: String, inOffset: Int): Int {
return if (vPrev < 0) 0 else vPrev + 1 return if (vPrev < 0) 0 else vPrev + 1
} }
/** Character offset of the end of the line containing [inOffset] — the position of /** Character offset of the end of the line containing [inOffset] - the position of
its '\n', or the length of [inText] if this is the last line. */ its '\n', or the length of [inText] if this is the last line. */
private fun lineEndAt(inText: String, inOffset: Int): Int { private fun lineEndAt(inText: String, inOffset: Int): Int {
val vClamped = inOffset.coerceIn(0, inText.length) val vClamped = inOffset.coerceIn(0, inText.length)
@@ -684,7 +684,7 @@ private fun lineEndAt(inText: String, inOffset: Int): Int {
return if (vNext < 0) inText.length else vNext return if (vNext < 0) inText.length else vNext
} }
/** Move a caret one visual "row" down while trying to keep its column — i.e. the /** Move a caret one visual "row" down while trying to keep its column - i.e. the
number of chars past the current line's start. If the target line is shorter, number of chars past the current line's start. If the target line is shorter,
the caret snaps to its end. Off-by-one at EOF collapses to the body length. */ the caret snaps to its end. Off-by-one at EOF collapses to the body length. */
private fun moveDown(inText: String, inOffset: Int): Int { private fun moveDown(inText: String, inOffset: Int): Int {
@@ -728,7 +728,7 @@ private fun wordRangeAt(inText: String, inOffset: Int): IntRange {
} }
/** The index of the chunk in [inChunks] whose character range contains /** The index of the chunk in [inChunks] whose character range contains
[inOffset], or null when the body is empty. Uses a linear scan — the chunk [inOffset], or null when the body is empty. Uses a linear scan - the chunk
count is O(body / kLinesPerChunk) which stays modest even for huge bodies. */ count is O(body / kLinesPerChunk) which stays modest even for huge bodies. */
private fun chunkContaining(inChunks: List<BodyChunk>, inOffset: Int): Int? { private fun chunkContaining(inChunks: List<BodyChunk>, inOffset: Int): Int? {
if (inChunks.isEmpty()) return null if (inChunks.isEmpty()) return null
@@ -791,7 +791,7 @@ private fun BodyChunkRow(
// Precompute this block's slice of the selection in LOCAL offsets. `null` // Precompute this block's slice of the selection in LOCAL offsets. `null`
// when the selection is empty or doesn't touch this block. Also computes the // when the selection is empty or doesn't touch this block. Also computes the
// caret's local offset when the caret itself falls inside this block (drawn // caret's local offset when the caret itself falls inside this block (drawn
// even when the range is empty — that's the read-only viewer's cursor). // even when the range is empty - that's the read-only viewer's cursor).
val vChunkStart = inChunk.startCharOffset val vChunkStart = inChunk.startCharOffset
val vChunkEnd = vChunkStart + inChunk.body.length val vChunkEnd = vChunkStart + inChunk.body.length
val vLocalRange: IntRange? = if (inSel == null || inSel.isEmpty) null else { val vLocalRange: IntRange? = if (inSel == null || inSel.isEmpty) null else {
@@ -886,7 +886,7 @@ private class BodyChunk(
/** Slice a highlighted body into blocks of [inLinesPerChunk] lines. The O(total-spans) /** Slice a highlighted body into blocks of [inLinesPerChunk] lines. The O(total-spans)
subSequence runs once per block HERE (build time, memoised) instead of once per subSequence runs once per block HERE (build time, memoised) instead of once per
visible line every frame — the difference between smooth and janky on a big body. */ visible line every frame - the difference between smooth and janky on a big body. */
private fun buildBodyChunks(inText: String, inAnn: AnnotatedString, inLinesPerChunk: Int): List<BodyChunk> { private fun buildBodyChunks(inText: String, inAnn: AnnotatedString, inLinesPerChunk: Int): List<BodyChunk> {
val vStarts = lineStartOffsets(inText) val vStarts = lineStartOffsets(inText)
val vN = vStarts.size val vN = vStarts.size
@@ -908,7 +908,7 @@ private fun buildBodyChunks(inText: String, inAnn: AnnotatedString, inLinesPerCh
/** Offsets where each source line starts (line i spans [starts[ i], starts[i+1]-1), /** Offsets where each source line starts (line i spans [starts[ i], starts[i+1]-1),
the -1 dropping the '\n'; the last line runs to the string end). One cheap O(n) the -1 dropping the '\n'; the last line runs to the string end). One cheap O(n)
scan, memoised — replaces the old gutter pre-pass that measured wrapping on scan, memoised - replaces the old gutter pre-pass that measured wrapping on
every one of N lines on each width change (the other half of the freeze). */ every one of N lines on each width change (the other half of the freeze). */
private fun lineStartOffsets(inText: String): IntArray { private fun lineStartOffsets(inText: String): IntArray {
val vStarts = ArrayList<Int>(64) val vStarts = ArrayList<Int>(64)
@@ -943,7 +943,7 @@ internal fun HeaderTable(inHeaders: List<Pair<String, String>>, modifier: Modifi
} }
} }
/** Body view with line numbers in the gutter — mimics the code panel /** Body view with line numbers in the gutter - mimics the code panel
in httpie. Numbers are dim; body text uses the regular colour. */ in httpie. Numbers are dim; body text uses the regular colour. */
@Composable @Composable
internal fun BodyView( internal fun BodyView(
@@ -963,7 +963,7 @@ internal fun BodyView(
val vGutterWidth = (vDigits * 7 + 4).dp val vGutterWidth = (vDigits * 7 + 4).dp
// Body wrap width (physical px, from onSizeChanged), reported by the body // Body wrap width (physical px, from onSizeChanged), reported by the body
// Box once it's laid out (0 on the first frame). The gutter numbering // Box once it's laid out (0 on the first frame). The gutter numbering
// depends on it — see vNumbers. Wrap measurement expects the font // depends on it - see vNumbers. Wrap measurement expects the font
// size + max-width in the SAME unit (both physical px on Retina), so we // size + max-width in the SAME unit (both physical px on Retina), so we
// scale 12.sp through the current density here, matching what the paragraph // scale 12.sp through the current density here, matching what the paragraph
// engine does when it builds the body's real paragraph. // engine does when it builds the body's real paragraph.
@@ -972,16 +972,16 @@ internal fun BodyView(
// Horizontal scroll for no-wrap mode (read-only body only); dormant when wrapping. // Horizontal scroll for no-wrap mode (read-only body only); dormant when wrapping.
val vHScroll = rememberScrollState() val vHScroll = rememberScrollState()
Row(modifier = modifier) { Row(modifier = modifier) {
// Numbers are reference-only — half-alpha so they stay legible without // Numbers are reference-only - half-alpha so they stay legible without
// competing with the body. Rendered as ONE '\n'-joined multi-line Text // competing with the body. Rendered as ONE '\n'-joined multi-line Text
// (not one Text per line): a 1000-line gutter is then a single node the // (not one Text per line): a 1000-line gutter is then a single node the
// renderer line-culls + wrap-caches exactly like the body, instead of // renderer line-culls + wrap-caches exactly like the body, instead of
// 1000 leaf nodes — which lagged and tripped SDL's ~16384px draw- // 1000 leaf nodes - which lagged and tripped SDL's ~16384px draw-
// coordinate limit (line numbers vanished past ~955). // coordinate limit (line numbers vanished past ~955).
// //
// When the body soft-wraps, a source line can span several visual rows. // When the body soft-wraps, a source line can span several visual rows.
// To keep the numbers aligned we emit each line's number once (on its // To keep the numbers aligned we emit each line's number once (on its
// first row) followed by one blank row per wrapped continuation — by // first row) followed by one blank row per wrapped continuation - by
// re-running the SAME wrap the body uses (same measurer, font, width), // re-running the SAME wrap the body uses (same measurer, font, width),
// so the gutter ends up with exactly as many rows as the body. Without // so the gutter ends up with exactly as many rows as the body. Without
// this, a line wrapping into 3 rows would read 10/11/12 instead of // this, a line wrapping into 3 rows would read 10/11/12 instead of
@@ -997,7 +997,7 @@ internal fun BodyView(
append(vIdx + 1) append(vIdx + 1)
if (vWrapRows) { if (vWrapRows) {
// Pass the density-scaled font size so this wrap matches the body's // Pass the density-scaled font size so this wrap matches the body's
// actual glyph pixel widths — passing 12 (sp) here counted each // actual glyph pixel widths - passing 12 (sp) here counted each
// char as half its rendered size, so long lines that visually // char as half its rendered size, so long lines that visually
// wrapped into 3 rows in the body were estimated as 1 or 2 rows // wrapped into 3 rows in the body were estimated as 1 or 2 rows
// here and the gutter numbers drifted upward. // here and the gutter numbers drifted upward.
@@ -1033,7 +1033,7 @@ internal fun BodyView(
} }
if (inOnChange != null) { if (inOnChange != null) {
// EDITABLE request body: BasicTextField with a colour-only // EDITABLE request body: BasicTextField with a colour-only
// syntax-highlight visualTransform — cursor / selection map to // syntax-highlight visualTransform - cursor / selection map to
// the plain text. RAW = no highlight. Remembered per (format, // the plain text. RAW = no highlight. Remembered per (format,
// theme) so typing and cursor-blink reuse it. // theme) so typing and cursor-blink reuse it.
val vDark = isDarkBg(c.bg) val vDark = isDarkBg(c.bg)
@@ -1079,7 +1079,7 @@ internal fun BodyView(
} }
SelectionContainer { SelectionContainer {
// No-wrap mode: one source line per row, long lines overflow // No-wrap mode: one source line per row, long lines overflow
// and the body pans horizontally (the gutter stays pinned — // and the body pans horizontally (the gutter stays pinned -
// it's outside this scroll). The scroll must sit on a PARENT // it's outside this scroll). The scroll must sit on a PARENT
// of the Text: a node's own scroll offset shifts its children, // of the Text: a node's own scroll offset shifts its children,
// not itself. Wrap mode: plain soft-wrapping Text. // not itself. Wrap mode: plain soft-wrapping Text.
@@ -1138,12 +1138,12 @@ internal fun ViewerTab(
} }
// ================== // ==================
// MARK: Helpers — header parsing / formatting // MARK: Helpers - header parsing / formatting
// ================== // ==================
/** "HTTP/1.1 200 OK" status line for a Response, with the protocol /** "HTTP/1.1 200 OK" status line for a Response, with the protocol
token dimmed (it's structural) and the status code + reason in the token dimmed (it's structural) and the status code + reason in the
status colour. Just "HTTP/1.1" pre-response — no placeholder. Triple- status colour. Just "HTTP/1.1" pre-response - no placeholder. Triple-
spaced to match the request-line formatting. */ spaced to match the request-line formatting. */
internal fun formatStatusLine(inResp: ApiResponse?, inColors: AppColors): AnnotatedString { internal fun formatStatusLine(inResp: ApiResponse?, inColors: AppColors): AnnotatedString {
if (inResp == null) return buildAnnotatedString { if (inResp == null) return buildAnnotatedString {
@@ -1197,7 +1197,7 @@ internal fun formatRequestLine(inReq: ApiRequest, inHttpVersion: String, inColor
} }
/** Pull the path (+ query) out of a URL, dropping scheme + host. The /** Pull the path (+ query) out of a URL, dropping scheme + host. The
request line in HTTP is "METHOD path HTTP/x.y" — the host lives on a request line in HTTP is "METHOD path HTTP/x.y" - the host lives on a
separate Host: header line, not in the path. Falls back to the input separate Host: header line, not in the path. Falls back to the input
string when no "://" is present (relative URL). */ string when no "://" is present (relative URL). */
internal fun urlPathOnly(inUrl: String): String { internal fun urlPathOnly(inUrl: String): String {
@@ -1208,7 +1208,7 @@ internal fun urlPathOnly(inUrl: String): String {
return if (vSlash < 0) "/" else vAfterScheme.substring(vSlash) return if (vSlash < 0) "/" else vAfterScheme.substring(vSlash)
} }
/** Extract the host (+ port) from a URL — used to synthesize a Host /** Extract the host (+ port) from a URL - used to synthesize a Host
header for display when the engine didn't surface one. */ header for display when the engine didn't surface one. */
internal fun urlHost(inUrl: String): String? { internal fun urlHost(inUrl: String): String? {
val vIdx = inUrl.indexOf("://") val vIdx = inUrl.indexOf("://")
@@ -1219,7 +1219,7 @@ internal fun urlHost(inUrl: String): String? {
return vAuthority.ifEmpty { null } return vAuthority.ifEmpty { null }
} }
/** The user-agent string our Darwin engine sends is opaque to Ktor — /** The user-agent string our Darwin engine sends is opaque to Ktor -
NSURLSession picks the default. Match what httpie does: identify NSURLSession picks the default. Match what httpie does: identify
ourselves so the wire log isn't missing the field entirely. */ ourselves so the wire log isn't missing the field entirely. */
internal const val kUserAgent: String = "compose-apidemo/1.0" internal const val kUserAgent: String = "compose-apidemo/1.0"
@@ -1248,7 +1248,7 @@ internal fun synthesizeRequestHeaders(
/** Body length in bytes for the headers synthesis. JSON / TEXT use the /** Body length in bytes for the headers synthesis. JSON / TEXT use the
raw UTF-8 byte count; FORM serialises and counts; FILE skips raw UTF-8 byte count; FORM serialises and counts; FILE skips
(loading the file just for the count would be wasteful — the engine (loading the file just for the count would be wasteful - the engine
sets the field anyway). */ sets the field anyway). */
internal fun computedBodyLength(inReq: ApiRequest): Int? = when (inReq.bodyType) { internal fun computedBodyLength(inReq: ApiRequest): Int? = when (inReq.bodyType) {
BodyType.TEXT -> inReq.body.encodeToByteArray().size BodyType.TEXT -> inReq.body.encodeToByteArray().size
@@ -1267,12 +1267,12 @@ internal fun formatTimingSize(inResp: ApiResponse): String {
} }
// ================== // ==================
// MARK: ViewerOverflowMenu — 3-dot menu in the bottom-right of the viewer // MARK: ViewerOverflowMenu - 3-dot menu in the bottom-right of the viewer
// ================== // ==================
/** Replaces the inline Copy / Save chips with a single MoreHoriz menu. /** Replaces the inline Copy / Save chips with a single MoreHoriz menu.
Copy actions target whichever tab is showing (request or response); Copy actions target whichever tab is showing (request or response);
Clear is global — wipes the response, sentReq, preview state, and any Clear is global - wipes the response, sentReq, preview state, and any
memory-backed image resource for the current request. */ memory-backed image resource for the current request. */
@Composable @Composable
internal fun ViewerOverflowMenu( internal fun ViewerOverflowMenu(
@@ -1370,7 +1370,7 @@ internal fun ViewerOverflowMenu(
} }
} }
/** Perceptual luminance check — true when the colour reads as "dark" /** Perceptual luminance check - true when the colour reads as "dark"
(background gets a light foreground). Standard Rec. 709 weights. */ (background gets a light foreground). Standard Rec. 709 weights. */
internal fun isDarkBg(inColor: Color): Boolean { internal fun isDarkBg(inColor: Color): Boolean {
val vY = 0.299f * inColor.red + 0.587f * inColor.green + 0.114f * inColor.blue val vY = 0.299f * inColor.red + 0.587f * inColor.green + 0.114f * inColor.blue
@@ -1378,7 +1378,7 @@ internal fun isDarkBg(inColor: Color): Boolean {
} }
// ================== // ==================
// MARK: BodyFormatSelector — small dropdown for RAW / JSON / XML / YAML / HTML // MARK: BodyFormatSelector - small dropdown for RAW / JSON / XML / YAML / HTML
// ================== // ==================
/** Format / "type" picker. inBordered renders it as a full dropdown matching /** Format / "type" picker. inBordered renders it as a full dropdown matching
@@ -1423,8 +1423,8 @@ internal fun BodyFormatSelector(
/** TLS validation indicator: only true when the URL is https AND we got /** TLS validation indicator: only true when the URL is https AND we got
a real response back (i.e. the OS engine completed the TLS handshake a real response back (i.e. the OS engine completed the TLS handshake
without an error). The engines we ship — NSURLSession on macOS, without an error). The engines we ship - NSURLSession on macOS,
WinHttp on Windows, libcurl on Linux — all reject an untrusted WinHttp on Windows, libcurl on Linux - all reject an untrusted
certificate by default, so a non-error response is implicit proof certificate by default, so a non-error response is implicit proof
that the OS validated the chain. */ that the OS validated the chain. */
internal fun isTlsValidated(inUrl: String, inResp: ApiResponse?): Boolean { internal fun isTlsValidated(inUrl: String, inResp: ApiResponse?): Boolean {
@@ -1469,7 +1469,7 @@ internal fun ViewerEmpty(inIcon: Int, inText: String, inModifier: Modifier = Mod
internal fun headersText(inHeaders: List<Pair<String, String>>): String = internal fun headersText(inHeaders: List<Pair<String, String>>): String =
inHeaders.joinToString("\n") { (vK, vV) -> "$vK: $vV" }.ifEmpty { "(no headers)" } inHeaders.joinToString("\n") { (vK, vV) -> "$vK: $vV" }.ifEmpty { "(no headers)" }
/** The headers that *would* be sent — explicit enabled headers plus the inferred /** The headers that *would* be sent - explicit enabled headers plus the inferred
Content-Type for the body type (unless one is already set). Used for Preview; Content-Type for the body type (unless one is already set). Used for Preview;
a sent request shows the real headers via headersText(response.requestHeaders). */ a sent request shows the real headers via headersText(response.requestHeaders). */
internal fun requestHeadersText(inReq: ApiRequest): String { internal fun requestHeadersText(inReq: ApiRequest): String {
@@ -9,7 +9,7 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
// ================== // ==================
// MARK: Voltic themes — selectable Material 3 palettes // MARK: Voltic themes - selectable Material 3 palettes
// ================== // ==================
// The three "voltic" brand palettes (Kotlin purple, orange, and a stock-M3 // The three "voltic" brand palettes (Kotlin purple, orange, and a stock-M3
// purple), each a full light + dark M3 ColorScheme plus an EXTENDED group that // purple), each a full light + dark M3 ColorScheme plus an EXTENDED group that
@@ -11,7 +11,7 @@ import okio.FileSystem
// MARK: Platform seams (native = com.compose.sdl / SDL3, jvm = AWT + upstream desktop) // MARK: Platform seams (native = com.compose.sdl / SDL3, jvm = AWT + upstream desktop)
// ================== // ==================
// The port modules are native-only, so shared code can't call com.compose.sdl // The port modules are native-only, so shared code can't call com.compose.sdl
// directly — every SDL-backed API the app uses goes through these expects. // directly - every SDL-backed API the app uses goes through these expects.
// Signatures mirror the native originals so the native actuals are pure // Signatures mirror the native originals so the native actuals are pure
// delegation (same pattern as :demo's demo.shim package). // delegation (same pattern as :demo's demo.shim package).
@@ -21,7 +21,7 @@ expect fun appDataDir(inOrg: String, inApp: String): String?
/** Open the OS file manager showing the given path. Fire-and-forget. */ /** Open the OS file manager showing the given path. Fire-and-forget. */
expect fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)? = null) expect fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)? = null)
/** "Explorer" / "Finder" / "Files" — for menu labels. */ /** "Explorer" / "Finder" / "Files" - for menu labels. */
expect fun fileManagerName(): String expect fun fileManagerName(): String
/** Async save-file dialog; the callback gets the chosen absolute path or null. */ /** Async save-file dialog; the callback gets the chosen absolute path or null. */
@@ -54,7 +54,7 @@ inFontPx (already density-scaled). Used by the body gutter so its line
numbers stay aligned with the wrapped body text. */ numbers stay aligned with the wrapped body text. */
expect fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int expect fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int
/** Editor tab width in spaces — how wide a typed '\t' renders. On the native /** Editor tab width in spaces - how wide a typed '\t' renders. On the native
stack this drives the project text pipeline (TextLayoutConfig); the jvm stack this drives the project text pipeline (TextLayoutConfig); the jvm
parity app only stores the preference. */ parity app only stores the preference. */
expect var editorTabWidth: Int expect var editorTabWidth: Int
@@ -63,10 +63,10 @@ expect var editorTabWidth: Int
// Infrastructure // Infrastructure
/** okio's FileSystem.SYSTEM is declared per-platform, not in its common /** okio's FileSystem.SYSTEM is declared per-platform, not in its common
metadata — surface it through a seam. */ metadata - surface it through a seam. */
internal expect val systemFileSystem: FileSystem internal expect val systemFileSystem: FileSystem
/** The app's Ktor client: Curl on native (bundled libcurl — same TLS stack as /** The app's Ktor client: Curl on native (bundled libcurl - same TLS stack as
the mTLS path), CIO on the jvm parity target. */ the mTLS path), CIO on the jvm parity target. */
expect fun createApiHttpClient(): HttpClient expect fun createApiHttpClient(): HttpClient
@@ -1,5 +1,5 @@
// COPY of the port's Scrollbar (package com.compose.sdl.scrollbar) so the SAME // COPY of the port's Scrollbar (package com.compose.sdl.scrollbar) so the SAME
// implementation runs on the jvm parity target — the port modules are // implementation runs on the jvm parity target - the port modules are
// native-only, so apidemo's commonMain cannot reference them directly. // native-only, so apidemo's commonMain cannot reference them directly.
// Keep in sync with the original when it changes. // Keep in sync with the original when it changes.
package apidemo.compat package apidemo.compat
@@ -41,7 +41,7 @@ import kotlin.math.roundToInt
/** A faithful subset of Compose Desktop's scrollbar (androidx.compose.foundation). /** A faithful subset of Compose Desktop's scrollbar (androidx.compose.foundation).
You overlay a VerticalScrollbar / HorizontalScrollbar on a scrollable and feed You overlay a VerticalScrollbar / HorizontalScrollbar on a scrollable and feed
it a ScrollbarAdapter built from the same ScrollState. This project's Box has it a ScrollbarAdapter built from the same ScrollState. This project's Box has
no BoxScope (so no Modifier.align) — pin the bar with the Box's no BoxScope (so no Modifier.align) - pin the bar with the Box's
contentAlignment; the fillMaxSize content fills the rest: contentAlignment; the fillMaxSize content fills the rest:
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
@@ -100,7 +100,7 @@ interface ScrollbarAdapter {
/** Adapter for a virtualized LazyColumn / LazyRow. A lazy list only knows the /** Adapter for a virtualized LazyColumn / LazyRow. A lazy list only knows the
size of its VISIBLE items, so exact pixel geometry of off-screen content is size of its VISIBLE items, so exact pixel geometry of off-screen content is
unknowable — we estimate it from the average visible item size × total item unknowable - we estimate it from the average visible item size × total item
count (the same approach as Compose Desktop). Accurate enough for a thumb count (the same approach as Compose Desktop). Accurate enough for a thumb
whose length/position track the scroll position; the estimate self-corrects whose length/position track the scroll position; the estimate self-corrects
as items of different sizes scroll through the viewport. */ as items of different sizes scroll through the viewport. */
@@ -131,7 +131,7 @@ private class LazyListScrollbarAdapter(private val state: LazyListState) : Scrol
// Within a viewport, scroll by EXACT pixels so a drag stays smooth. A lazy // Within a viewport, scroll by EXACT pixels so a drag stays smooth. A lazy
// list's pixel geometry is only ESTIMATED (average visible item size), and // list's pixel geometry is only ESTIMATED (average visible item size), and
// that estimate drifts as differently-sized items scroll through the // that estimate drifts as differently-sized items scroll through the
// viewport — so index-snapping every frame makes the thumb jump. scrollBy // viewport - so index-snapping every frame makes the thumb jump. scrollBy
// sidesteps the estimate entirely. This is what Compose Desktop's own // sidesteps the estimate entirely. This is what Compose Desktop's own
// LazyListScrollbarAdapter does, and for the same reason. // LazyListScrollbarAdapter does, and for the same reason.
if (abs(vDistance) <= viewportSize) { if (abs(vDistance) <= viewportSize) {
@@ -184,7 +184,7 @@ private fun Scrollbar(
val vHovered by vHoverSource.collectIsHoveredAsState() val vHovered by vHoverSource.collectIsHoveredAsState()
var vDraggingThumb by remember { mutableStateOf(false) } var vDraggingThumb by remember { mutableStateOf(false) }
// onSizeChanged reports PHYSICAL pixels (Option-B density flow), so every // onSizeChanged reports PHYSICAL pixels (Option-B density flow), so every
// int/px carried through here — vTrackPx, vThumbLen, vThumbPos — is in // int/px carried through here - vTrackPx, vThumbLen, vThumbPos - is in
// physical pixels. Passing them into `.dp` doubles again through // physical pixels. Passing them into `.dp` doubles again through
// density.toPx() on Retina, which visibly bloated the thumb (filled the // density.toPx() on Retina, which visibly bloated the thumb (filled the
// whole track) and made drag/track-click positions land 2× too far down. // whole track) and made drag/track-click positions land 2× too far down.
@@ -230,13 +230,13 @@ private fun Scrollbar(
Box(modifier = vThumbMod) Box(modifier = vThumbMod)
// ============ // ============
// Drag the thumb / page on track click — handled on the track so the // Drag the thumb / page on track click - handled on the track so the
// coordinates have a stable (non-moving) origin. The press is CONSUMED: // coordinates have a stable (non-moving) origin. The press is CONSUMED:
// a scrollbar owns the pointer input it handles, so content sharing this // a scrollbar owns the pointer input it handles, so content sharing this
// region (the scrollable beneath, or a parent that also reads pointer // region (the scrollable beneath, or a parent that also reads pointer
// input) never reacts to the same press. Geometry is read LIVE from the // input) never reacts to the same press. Geometry is read LIVE from the
// adapter at press time — not from the captured render metrics above, // adapter at press time - not from the captured render metrics above,
// which go stale between pointerInput relaunches — so a grab after a // which go stale between pointerInput relaunches - so a grab after a
// wheel-scroll still hit-tests against the thumb's current position. // wheel-scroll still hit-tests against the thumb's current position.
Box( Box(
modifier = Modifier modifier = Modifier
@@ -246,7 +246,7 @@ private fun Scrollbar(
val vDown = awaitFirstDown(requireUnconsumed = false) val vDown = awaitFirstDown(requireUnconsumed = false)
vDown.consume() vDown.consume()
// Live geometry — stable for the drag's duration (content / // Live geometry - stable for the drag's duration (content /
// viewport / track sizes don't change mid-drag). // viewport / track sizes don't change mid-drag).
val vContentPx = adapter.contentSize val vContentPx = adapter.contentSize
val vViewportPx = adapter.viewportSize val vViewportPx = adapter.viewportSize
@@ -262,7 +262,7 @@ private fun Scrollbar(
// Map a track-relative thumb-top to a scroll offset. The // Map a track-relative thumb-top to a scroll offset. The
// target uses the LIVE max scroll (read fresh each move), // target uses the LIVE max scroll (read fresh each move),
// not the press-time value — the adapter compares it against // not the press-time value - the adapter compares it against
// its live scrollOffset, and mixing a stale scale with a live // its live scrollOffset, and mixing a stale scale with a live
// one makes the delta run away (grab at the bottom, drag up, // one makes the delta run away (grab at the bottom, drag up,
// and the list overshoots to the middle). // and the list overshoots to the middle).
@@ -1,5 +1,5 @@
// COPY of the port's SplitPane (package com.compose.sdl.widgets) so the SAME // COPY of the port's SplitPane (package com.compose.sdl.widgets) so the SAME
// implementation runs on the jvm parity target — the port modules are // implementation runs on the jvm parity target - the port modules are
// native-only, so apidemo's commonMain cannot reference them directly. // native-only, so apidemo's commonMain cannot reference them directly.
// Keep in sync with the original when it changes. // Keep in sync with the original when it changes.
package apidemo.compat package apidemo.compat
@@ -66,7 +66,7 @@ fun HorizontalSplitPane(
Box(modifier = Modifier.width(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxHeight()) { first() } Box(modifier = Modifier.width(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxHeight()) { first() }
// Solid divider that fills its own slot — what you see is exactly what // Solid divider that fills its own slot - what you see is exactly what
// you can grab. Hover or an in-progress drag just changes the colour (no // you can grab. Hover or an in-progress drag just changes the colour (no
// size change → no layout shift, and no flicker when the pointer briefly // size change → no layout shift, and no flicker when the pointer briefly
// leaves the slot mid-drag). // leaves the slot mid-drag).
+14 -2
View File
@@ -4,14 +4,14 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.platform.Font import androidx.compose.ui.text.platform.Font
// ================== // ==================
// MARK: Monospace body font — JVM actuals (classpath font/ staged by // MARK: Monospace body font - JVM actuals (classpath font/ staged by
// jvmProcessResources) // jvmProcessResources)
// ================== // ==================
actual val monoFontFamily: FontFamily? by lazy { actual val monoFontFamily: FontFamily? by lazy {
val vBytes = object {}.javaClass.getResourceAsStream("/font/NotoSansMono.ttf")?.use { it.readBytes() } val vBytes = object {}.javaClass.getResourceAsStream("/font/NotoSansMono.ttf")?.use { it.readBytes() }
if (vBytes == null) { if (vBytes == null) {
println("apidemo: NotoSansMono.ttf not on the classpath — body uses the default font") println("apidemo: NotoSansMono.ttf not on the classpath - body uses the default font")
null null
} else { } else {
FontFamily(Font(identity = kMonoFamily, data = vBytes)) FontFamily(Font(identity = kMonoFamily, data = vBytes))
@@ -21,3 +21,15 @@ actual val monoFontFamily: FontFamily? by lazy {
actual val monoFontFamilyName: String? by lazy { actual val monoFontFamilyName: String? by lazy {
if (monoFontFamily != null) kMonoFamily else null if (monoFontFamily != null) kMonoFamily else null
} }
/** The JVM leg would otherwise inherit a SYSTEM font here; load the same bundled
NotoSans the native build uses so the two stacks are comparable. */
actual val defaultFontFamily: FontFamily? by lazy {
val vBytes = object {}.javaClass.getResourceAsStream("/font/NotoSans.ttf")?.use { it.readBytes() }
if (vBytes == null) {
println("apidemo: NotoSans.ttf not on the classpath - using the platform default font")
null
} else {
FontFamily(Font(identity = kDefaultFamily, data = vBytes))
}
}
+78 -3
View File
@@ -1,19 +1,48 @@
package apidemo package apidemo
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.ImageComposeScene
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.InfiniteAnimationPolicy
import androidx.compose.ui.res.useResource import androidx.compose.ui.res.useResource
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import apidemo.compat.jvmOnCloseRequest
import apidemo.compat.jvmOnKeyShortcut
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import org.jetbrains.compose.resources.decodeToImageBitmap import org.jetbrains.compose.resources.decodeToImageBitmap
import org.jetbrains.skia.EncodedImageFormat
import java.io.File
// The JVM comparison app's entry point. Runs the SAME shared App() as the // The JVM comparison app's entry point. Runs the SAME shared App() as the
// native :apidemo, but on Compose Desktop (JVM) against upstream // native :apidemo, but on Compose Desktop (JVM) against upstream
// org.jetbrains.compose. Client-certificate (mTLS) features are native-only // org.jetbrains.compose. Client-certificate (mTLS) features are native-only
// (they drive the bundled libcurl); the jvm actuals report that instead. // (they drive the bundled libcurl); the jvm actuals report that instead.
fun main() = application { //
// --screenshot=<path.png> render headlessly to quiescence, write, exit
//
// The screenshot mode is the JVM half of the native-vs-JVM drift check; the
// native half is `apidemo.exe --screenshot=<path.bmp>`. Both render offscreen
// (ImageComposeScene here, a GPU readback there), so neither needs a visible
// window and the comparison is unaffected by whatever is on screen.
fun main(args: Array<String>) {
val screenshot = args.firstOrNull { it.startsWith("--screenshot=") }?.substringAfter('=')
if (screenshot != null) {
screenshotApp(File(screenshot), kWidth, kHeight)
return
}
runInteractive()
}
private const val kWidth = 1240
private const val kHeight = 820
private fun runInteractive() = application {
// Voltic window icon (mirrors the native window icon). Staged onto the // Voltic window icon (mirrors the native window icon). Staged onto the
// classpath at icon/ by jvmProcessResources. // classpath at icon/ by jvmProcessResources.
val vIcon = remember { val vIcon = remember {
@@ -23,10 +52,56 @@ fun main() = application {
// The shared App installs a persist-then-close hook (InstallWindowHooks). // The shared App installs a persist-then-close hook (InstallWindowHooks).
onCloseRequest = { if (jvmOnCloseRequest?.invoke() != false) exitApplication() }, onCloseRequest = { if (jvmOnCloseRequest?.invoke() != false) exitApplication() },
onPreviewKeyEvent = { jvmOnKeyShortcut?.invoke(it) ?: false }, onPreviewKeyEvent = { jvmOnKeyShortcut?.invoke(it) ?: false },
title = "API Manager — JVM (upstream Compose)", title = "API Manager - JVM (upstream Compose)",
icon = vIcon, icon = vIcon,
state = rememberWindowState(width = 1240.dp, height = 820.dp), state = rememberWindowState(width = kWidth.dp, height = kHeight.dp),
) { ) {
App() App()
} }
} }
// ==================
// MARK: Headless screenshot (drift check vs the native leg)
// ==================
private const val kFrameNanos = 16_666_667L
private const val kMaxFrames = 300
/** Infinite animations would never let the scene go quiet; upstream's test policy
freezes them at their initial value (same trick :demo's parity leg uses). */
private object CancelInfiniteAnimations : InfiniteAnimationPolicy {
override suspend fun <R> onInfiniteOperation(block: suspend () -> R): R =
throw CancellationException("infinite animations disabled for the drift screenshot")
}
/** Render [App] offscreen at density 1 - matching the native leg, which lays out in
physical pixels - stepping a virtual 60fps clock until the scene stops invalidating. */
@OptIn(ExperimentalComposeUiApi::class)
private fun screenshotApp(outFile: File, width: Int, height: Int) {
outFile.parentFile?.mkdirs()
val scene = ImageComposeScene(
width, height, density = Density(1f),
coroutineContext = Dispatchers.Unconfined + CancelInfiniteAnimations,
) {
App()
}
try {
var nanos = 0L
var frames = 0
var image = scene.render(nanos)
while (scene.hasInvalidations() && frames < kMaxFrames) {
nanos += kFrameNanos
image = scene.render(nanos)
frames++
}
val png = image.encodeToData(EncodedImageFormat.PNG)
if (png == null) {
println("jvm screenshot: PNG encode failed")
return
}
outFile.writeBytes(png.bytes)
println("jvm screenshot: ${outFile.path} (settled after $frames frame(s))")
} finally {
scene.close()
}
}
+2 -2
View File
@@ -1,14 +1,14 @@
package apidemo package apidemo
// ================== // ==================
// MARK: TLS chain / client-certificate — JVM actuals (native-only feature) // MARK: TLS chain / client-certificate - JVM actuals (native-only feature)
// ================== // ==================
// The native builds drive the bundled libcurl (CURLOPT_SSLCERT / CERTINFO); // The native builds drive the bundled libcurl (CURLOPT_SSLCERT / CERTINFO);
// there is no libcurl on the jvm parity stack, so both entry points report // there is no libcurl on the jvm parity stack, so both entry points report
// the limitation instead of half-implementing it over JSSE. // the limitation instead of half-implementing it over JSSE.
private const val kNativeOnly = private const val kNativeOnly =
"Client-certificate (mTLS) features need the native build — they drive the bundled libcurl directly." "Client-certificate (mTLS) features need the native build - they drive the bundled libcurl directly."
actual fun inspectTlsChain(inReq: ApiRequest): TlsChain = TlsChain(emptyList(), kNativeOnly) actual fun inspectTlsChain(inReq: ApiRequest): TlsChain = TlsChain(emptyList(), kNativeOnly)
@@ -19,6 +19,7 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import apidemo.monoFontFamily
import org.jetbrains.compose.resources.decodeToSvgPainter import org.jetbrains.compose.resources.decodeToSvgPainter
import org.jetbrains.skia.Image import org.jetbrains.skia.Image
import java.awt.Desktop import java.awt.Desktop
@@ -29,7 +30,7 @@ import java.awt.datatransfer.StringSelection
import java.io.File import java.io.File
// ================== // ==================
// MARK: JVM actuals — AWT + upstream Compose Desktop // MARK: JVM actuals - AWT + upstream Compose Desktop
// ================== // ==================
private val kIsWindows = System.getProperty("os.name").startsWith("Windows") private val kIsWindows = System.getProperty("os.name").startsWith("Windows")
@@ -41,7 +42,7 @@ actual fun createApiHttpClient(): io.ktor.client.HttpClient =
io.ktor.client.HttpClient(io.ktor.client.engine.cio.CIO) io.ktor.client.HttpClient(io.ktor.client.engine.cio.CIO)
// ============ // ============
// Window hooks — the shared App installs them; MainJvm wires them into the // Window hooks - the shared App installs them; MainJvm wires them into the
// upstream Window() (onCloseRequest / onPreviewKeyEvent). // upstream Window() (onCloseRequest / onPreviewKeyEvent).
internal var jvmOnCloseRequest: (() -> Boolean)? = null internal var jvmOnCloseRequest: (() -> Boolean)? = null
@@ -154,11 +155,11 @@ actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inF
).lineCount ).lineCount
}.getOrDefault(1) }.getOrDefault(1)
/** The jvm parity app only stores the preference — upstream's text pipeline has /** The jvm parity app only stores the preference - upstream's text pipeline has
no global tab-width knob. Backed by state so menu checkmarks recompose. */ no global tab-width knob. Backed by state so menu checkmarks recompose. */
private var fTabWidth by mutableStateOf(4) private var mTabWidth by mutableStateOf(4)
actual var editorTabWidth: Int actual var editorTabWidth: Int
get() = fTabWidth get() = mTabWidth
set(value) { set(value) {
fTabWidth = value mTabWidth = value
} }
@@ -1,7 +1,7 @@
package apidemo package apidemo
// Linux: Ktor bundles an OpenSSL-backed libcurl, which reads PEM / DER / PKCS#12 // Linux: Ktor bundles an OpenSSL-backed libcurl, which reads PEM / DER / PKCS#12
// certificate and key files directly — no certificate-store dance needed. // certificate and key files directly - no certificate-store dance needed.
/** Point libcurl straight at the certificate / key files. */ /** Point libcurl straight at the certificate / key files. */
actual fun prepareClientCert(inReq: ApiRequest): PreparedCert = actual fun prepareClientCert(inReq: ApiRequest): PreparedCert =
@@ -17,6 +17,6 @@ actual fun prepareClientCert(inReq: ApiRequest): PreparedCert =
/** No temporary certificate store off Windows. */ /** No temporary certificate store off Windows. */
actual fun sweepTempClientCerts() {} actual fun sweepTempClientCerts() {}
/** No OS-store issuer resolution here — just continue with the name-only issuer. */ /** No OS-store issuer resolution here - just continue with the name-only issuer. */
actual fun extendChain(inServerCerts: List<List<Pair<String, String>>>): List<ChainCert> = actual fun extendChain(inServerCerts: List<List<Pair<String, String>>>): List<ChainCert> =
serverChainWithIssuerName(inServerCerts) serverChainWithIssuerName(inServerCerts)
@@ -11,14 +11,14 @@ import kotlin.io.encoding.Base64
// Windows certificate store, referenced by SHA-1 thumbprint. So for a request // Windows certificate store, referenced by SHA-1 thumbprint. So for a request
// that carries a certificate we import the cert + private key into the standard // that carries a certificate we import the cert + private key into the standard
// CurrentUser\MY ("Personal") store, hand curl "CurrentUser\MY\<thumbprint>", // CurrentUser\MY ("Personal") store, hand curl "CurrentUser\MY\<thumbprint>",
// and delete it again the instant the request finishes — nothing accumulates. // and delete it again the instant the request finishes - nothing accumulates.
// //
// Our entries are tagged (friendly name + key-container name) with a unique // Our entries are tagged (friendly name + key-container name) with a unique
// prefix so the startup sweep can clear crash leftovers without ever touching // prefix so the startup sweep can clear crash leftovers without ever touching
// the user's own certificates. // the user's own certificates.
// //
// Kotlin/Native maps several CryptoAPI LPCSTR params to String?, which makes the // Kotlin/Native maps several CryptoAPI LPCSTR params to String?, which makes the
// CryptDecodeObjectEx route (numeric struct-type sentinels) unusable — so the // CryptDecodeObjectEx route (numeric struct-type sentinels) unusable - so the
// RSA private key is parsed here (ASN.1) and laid out as a CryptoAPI // RSA private key is parsed here (ASN.1) and laid out as a CryptoAPI
// PRIVATEKEYBLOB directly, then handed to CryptImportKey. RSA (PKCS#1 or PKCS#8) // PRIVATEKEYBLOB directly, then handed to CryptImportKey. RSA (PKCS#1 or PKCS#8)
// and PKCS#12 are handled; EC PEM/DER is reported as unsupported with a hint. // and PKCS#12 are handled; EC PEM/DER is reported as unsupported with a hint.
@@ -42,7 +42,7 @@ actual fun prepareClientCert(inReq: ApiRequest): PreparedCert {
} }
return PreparedCert( return PreparedCert(
sslCert = "$kStorePath\\$vThumb", sslCert = "$kStorePath\\$vThumb",
sslCertType = null, // store reference — not a file type sslCertType = null, // store reference - not a file type
sslKey = null, sslKey = null,
sslKeyType = null, sslKeyType = null,
keyPassword = null, keyPassword = null,
@@ -62,7 +62,7 @@ private fun importPemDerRsa(inCertBytes: ByteArray, inReq: ApiRequest): Pair<Str
val vCertDer = derFromMaybePem(inCertBytes, "CERTIFICATE") val vCertDer = derFromMaybePem(inCertBytes, "CERTIFICATE")
val vKeyBytes = if (inReq.keyPath.isNotBlank()) readFileBytes(inReq.keyPath) else inCertBytes val vKeyBytes = if (inReq.keyPath.isNotBlank()) readFileBytes(inReq.keyPath) else inCertBytes
val vKeyDer = privateKeyDer(vKeyBytes) val vKeyDer = privateKeyDer(vKeyBytes)
?: throw RuntimeException("No private key found — PEM/DER on Windows needs the RSA key (a key file, or a key block in the certificate file).") ?: throw RuntimeException("No private key found - PEM/DER on Windows needs the RSA key (a key file, or a key block in the certificate file).")
val vBlob = try { val vBlob = try {
rsaKeyDerToBlob(vKeyDer) rsaKeyDerToBlob(vKeyDer)
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -374,7 +374,7 @@ private fun deleteContainer(inContainer: String) = memScoped {
CryptAcquireContextW(vProv.ptr, inContainer, kProvName, PROV_RSA_FULL.toUInt(), CRYPT_DELETEKEYSET.toUInt()) CryptAcquireContextW(vProv.ptr, inContainer, kProvName, PROV_RSA_FULL.toUInt(), CRYPT_DELETEKEYSET.toUInt())
} }
/** Remove every temp cert we ever added (prefix-tagged) plus its container — for /** Remove every temp cert we ever added (prefix-tagged) plus its container - for
clearing crash leftovers at startup. The user's own certs are never matched. */ clearing crash leftovers at startup. The user's own certs are never matched. */
@OptIn(ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
actual fun sweepTempClientCerts() { actual fun sweepTempClientCerts() {
@@ -417,7 +417,7 @@ private fun friendlyNameOf(inCert: CPointer<CERT_CONTEXT>): String? = memScoped
// Chain extension via the OS certificate store // Chain extension via the OS certificate store
// ============ // ============
/** Continue the server's chain by asking the OS to build it from the leaf — this /** Continue the server's chain by asking the OS to build it from the leaf - this
pulls intermediates/roots from the Windows store with full info. Falls back to pulls intermediates/roots from the Windows store with full info. Falls back to
a name-only issuer if the OS can't (or the leaf can't be parsed). */ a name-only issuer if the OS can't (or the leaf can't be parsed). */
@OptIn(ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
+10 -10
View File
@@ -11,7 +11,7 @@ import kotlin.time.TimeSource
// MARK: Client-certificate (mTLS) sending via libcurl // MARK: Client-certificate (mTLS) sending via libcurl
// ================== // ==================
// Ktor's native engines expose no client-certificate API, so a request that // Ktor's native engines expose no client-certificate API, so a request that
// carries a client certificate is sent here instead — straight through the // carries a client certificate is sent here instead - straight through the
// libcurl that ktor-client-curl already bundles (package `libcurl`, embedded // libcurl that ktor-client-curl already bundles (package `libcurl`, embedded
// static archive; Schannel on Windows, OpenSSL on macOS/Linux). This is the // static archive; Schannel on Windows, OpenSSL on macOS/Linux). This is the
// same TLS stack the default engine uses, so behaviour matches; we just get to // same TLS stack the default engine uses, so behaviour matches; we just get to
@@ -19,7 +19,7 @@ import kotlin.time.TimeSource
// //
// The transfer is synchronous (curl_easy_perform) and runs on Dispatchers.Default // The transfer is synchronous (curl_easy_perform) and runs on Dispatchers.Default
// from HttpRunner.run(); cancellation is best-effort (an in-flight perform can't // from HttpRunner.run(); cancellation is best-effort (an in-flight perform can't
// be interrupted — the result is simply discarded if the caller cancelled). // be interrupted - the result is simply discarded if the caller cancelled).
/** A client certificate readied for libcurl: the CURLOPT_SSLCERT value plus the /** A client certificate readied for libcurl: the CURLOPT_SSLCERT value plus the
other SSL options to set, and a cleanup to run once the request is done. other SSL options to set, and a cleanup to run once the request is done.
@@ -40,7 +40,7 @@ Throws with a user-facing message if the cert/key can't be loaded. */
expect fun prepareClientCert(inReq: ApiRequest): PreparedCert expect fun prepareClientCert(inReq: ApiRequest): PreparedCert
// ChainCert / TlsChain and the sweepTempClientCerts / inspectTlsChain / // ChainCert / TlsChain and the sweepTempClientCerts / inspectTlsChain /
// curlSendWithClientCert expects live in commonMain (TlsCommon.kt) — the UI // curlSendWithClientCert expects live in commonMain (TlsCommon.kt) - the UI
// consumes them from shared code; the per-OS ClientCert*.kt actuals below the // consumes them from shared code; the per-OS ClientCert*.kt actuals below the
// native tree actualize sweepTempClientCerts directly. // native tree actualize sweepTempClientCerts directly.
@@ -75,7 +75,7 @@ private class CurlSink {
val headerRaw = StringBuilder() // raw response header lines, all responses incl. redirects val headerRaw = StringBuilder() // raw response header lines, all responses incl. redirects
} }
/** Body write callback — appends each chunk to the sink's buffer. */ /** Body write callback - appends each chunk to the sink's buffer. */
@OptIn(ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
private fun onCurlBody( private fun onCurlBody(
inBuffer: CPointer<ByteVar>, inBuffer: CPointer<ByteVar>,
@@ -89,7 +89,7 @@ private fun onCurlBody(
return vLen.convert() return vLen.convert()
} }
/** Header callback — one parsed header line per call; accumulated for parsing. */ /** Header callback - one parsed header line per call; accumulated for parsing. */
@OptIn(ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
private fun onCurlHeader( private fun onCurlHeader(
inBuffer: CPointer<ByteVar>, inBuffer: CPointer<ByteVar>,
@@ -114,7 +114,7 @@ private fun onCurlDiscard(
): size_t = ): size_t =
(inSize * inCount) (inSize * inCount)
/** Open a TLS connection to the request's URL (handshake only — no body) and /** Open a TLS connection to the request's URL (handshake only - no body) and
return the server's certificate chain via CURLINFO_CERTINFO. Reuses the return the server's certificate chain via CURLINFO_CERTINFO. Reuses the
request's client certificate if it has one (so mTLS endpoints work too). request's client certificate if it has one (so mTLS endpoints work too).
inReq is already var-resolved. */ inReq is already var-resolved. */
@@ -294,7 +294,7 @@ actual fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse {
headers = parseRawHeaders(vSink.headerRaw.toString()), headers = parseRawHeaders(vSink.headerRaw.toString()),
body = when { body = when {
vIsImage -> "" vIsImage -> ""
vBinary -> "(${vCt ?: "binary"} · ${vBytes.size} bytes — not shown; use Save as…)" vBinary -> "(${vCt ?: "binary"} · ${vBytes.size} bytes - not shown; use Save as…)"
else -> vBytes.decodeToString() else -> vBytes.decodeToString()
}, },
bytes = vBytes, bytes = vBytes,
@@ -314,7 +314,7 @@ actual fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse {
private fun errorResponse(inMessage: String, inMs: Long): ApiResponse = private fun errorResponse(inMessage: String, inMs: Long): ApiResponse =
ApiResponse( ApiResponse(
status = 0, status = 0,
statusText = "—", statusText = "-",
timeMs = inMs, timeMs = inMs,
sizeBytes = 0, sizeBytes = 0,
headers = emptyList(), headers = emptyList(),
@@ -341,8 +341,8 @@ private fun requestBodyBytes(inReq: ApiRequest): CurlBody? {
} }
/** Parse libcurl's accumulated raw header text into ordered key/value pairs, /** Parse libcurl's accumulated raw header text into ordered key/value pairs,
keeping only the final response's block (a new "HTTP/" status line — emitted keeping only the final response's block (a new "HTTP/" status line - emitted
per redirect hop — resets what we've gathered). */ per redirect hop - resets what we've gathered). */
private fun parseRawHeaders(inRaw: String): List<Pair<String, String>> { private fun parseRawHeaders(inRaw: String): List<Pair<String, String>> {
var vCurrent = mutableListOf<Pair<String, String>>() var vCurrent = mutableListOf<Pair<String, String>>()
for (vRawLine in inRaw.split("\r\n", "\n")) { for (vRawLine in inRaw.split("\r\n", "\n")) {
+15 -2
View File
@@ -6,13 +6,13 @@ import com.compose.sdl.loadComposeResourceBytes
import com.compose.sdl.text.namedFontFamily import com.compose.sdl.text.namedFontFamily
// ================== // ==================
// MARK: Monospace body font — native actuals (data.kres + IconFont) // MARK: Monospace body font - native actuals (data.kres + IconFont)
// ================== // ==================
actual val monoFontFamilyName: String? by lazy { actual val monoFontFamilyName: String? by lazy {
val vBytes = loadComposeResourceBytes("font/NotoSansMono.ttf") val vBytes = loadComposeResourceBytes("font/NotoSansMono.ttf")
if (vBytes == null) { if (vBytes == null) {
println("apidemo: NotoSansMono.ttf not bundled — body uses the default font") println("apidemo: NotoSansMono.ttf not bundled - body uses the default font")
null null
} else { } else {
IconFont.register(kMonoFamily, vBytes) IconFont.register(kMonoFamily, vBytes)
@@ -23,3 +23,16 @@ actual val monoFontFamilyName: String? by lazy {
actual val monoFontFamily: FontFamily? by lazy { actual val monoFontFamily: FontFamily? by lazy {
monoFontFamilyName?.let { namedFontFamily(it) } monoFontFamilyName?.let { namedFontFamily(it) }
} }
/** Native already resolves FontFamily.Default to the bundled NotoSans, but the
family is registered by name so both stacks go through the same lookup. */
actual val defaultFontFamily: FontFamily? by lazy {
val vBytes = loadComposeResourceBytes("font/NotoSans.ttf")
if (vBytes == null) {
println("apidemo: NotoSans.ttf not bundled - using the platform default font")
null
} else {
IconFont.register(kDefaultFamily, vBytes)
namedFontFamily(kDefaultFamily)
}
}
+61 -4
View File
@@ -1,13 +1,19 @@
package apidemo package apidemo
import com.compose.sdl.AppWindowIcon import com.compose.sdl.AppWindowIcon
import com.compose.sdl.disableInfiniteAnimations
import com.compose.sdl.nativeComposeWindow import com.compose.sdl.nativeComposeWindow
import com.compose.sdl.useVirtualFrameTime
import com.compose.sdl.windowHasInvalidations
import kotlinx.cinterop.ExperimentalForeignApi
import utils.encodeBmpBgra32
import utils.writeFile
// ================== // ==================
// MARK: Entry point (native — SDL window shell) // MARK: Entry point (native - SDL window shell)
// ================== // ==================
// The voltic window/taskbar icon — pre-decoded .rgba blobs bundled into data.kres // The voltic window/taskbar icon - pre-decoded .rgba blobs bundled into data.kres
// under icon/ by the bridge plugin (compose.desktop.native { icon {} }); the // under icon/ by the bridge plugin (compose.desktop.native { icon {} }); the
// backend uses the largest as the base and the rest as alternate sizes. // backend uses the largest as the base and the rest as alternate sizes.
private val kAppIcon = AppWindowIcon( private val kAppIcon = AppWindowIcon(
@@ -15,6 +21,57 @@ private val kAppIcon = AppWindowIcon(
dark = listOf("icon/voltic-icon-dark-128.rgba", "icon/voltic-icon-dark-32.rgba") dark = listOf("icon/voltic-icon-dark-128.rgba", "icon/voltic-icon-dark-32.rgba")
) )
fun main() { private const val kWidth = 1240
nativeComposeWindow(title = "API Manager", width = 1240, height = 820, icon = kAppIcon) { App() } private const val kHeight = 820
/** Safety net for content that never stops invalidating. */
private const val kMaxFrames = 300
/**
* Entry point.
*
* `--screenshot=<path.bmp>` renders to quiescence and dumps the framebuffer, then
* exits - the native half of the JVM-vs-native drift check (the JVM half is
* `./gradlew :apidemo:run --args=--screenshot=<path.png>`, which goes through
* ImageComposeScene). The capture is a GPU READBACK, not a screen grab, so it
* does not care whether the window is visible or occluded.
*/
@OptIn(ExperimentalForeignApi::class)
fun main(args: Array<String>) {
val screenshot = args.firstOrNull { it.startsWith("--screenshot=") }?.substringAfter('=')
if (screenshot != null) {
// Match the JVM leg: freeze infinite animations and drive a virtual clock so
// entrance animations settle deterministically instead of run-to-run.
disableInfiniteAnimations = true
useVirtualFrameTime = true
}
nativeComposeWindow(
title = "API Manager",
width = kWidth,
height = kHeight,
icon = kAppIcon,
onFrame = if (screenshot != null) {
// Capture once the window reports no pending invalidations for a few
// consecutive frames, or at the cap. Mirrors :demo's parity path.
var quietFrames = 0
{ backend, frameIndex ->
quietFrames = if (windowHasInvalidations()) 0 else quietFrames + 1
if (quietFrames >= 3 || frameIndex >= kMaxFrames) {
if (frameIndex >= kMaxFrames) {
println("Screenshot: quiescence not reached by frame $frameIndex - capturing anyway")
}
val snap = backend.snapshotBgra()
if (snap != null) {
val (w, h, bgra) = snap
writeFile(screenshot, encodeBmpBgra32(w, h, bgra))
println("Wrote screenshot: $screenshot (${w}x$h, settled at frame $frameIndex)")
} else {
println("Screenshot snapshot was null")
}
false // quit
} else true
}
} else null,
) { App() }
} }
@@ -28,7 +28,7 @@ import io.ktor.client.engine.curl.*
import okio.FileSystem import okio.FileSystem
// ================== // ==================
// MARK: Native actuals — pure delegation to the port's SDL-backed APIs // MARK: Native actuals - pure delegation to the port's SDL-backed APIs
// ================== // ==================
actual fun appDataDir(inOrg: String, inApp: String): String? = actual fun appDataDir(inOrg: String, inApp: String): String? =
@@ -55,7 +55,7 @@ actual fun removeMemoryImage(inKey: String) = removeMemoryResource(inKey)
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter = actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter =
painterResource(inKey, if (inSvg) ResourceKind.Svg else ResourceKind.Raster) painterResource(inKey, if (inSvg) ResourceKind.Svg else ResourceKind.Raster)
// Cached resolver for wrappedRowCount — the skiko engine resolves fonts by name // Cached resolver for wrappedRowCount - the skiko engine resolves fonts by name
// (projectFontName), so this is just the required non-null Paragraph argument. // (projectFontName), so this is just the required non-null Paragraph argument.
private val mRowCountFontResolver = createFontFamilyResolver() private val mRowCountFontResolver = createFontFamilyResolver()
@@ -0,0 +1,83 @@
package utils
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.refTo
import platform.posix.fclose
import platform.posix.fopen
import platform.posix.fwrite
// ==================
// MARK: BMP save
// ==================
// Copy of demo/src/nativeMain/kotlin/utils/Bmp.kt. Kept duplicated rather than
// promoted into a library module: it is app-level tooling for the drift-check
// screenshots, and :ui-graphics deliberately does not re-export skiko to apps,
// so there is no natural shared home that would not widen the published API.
// ==================
@OptIn(ExperimentalForeignApi::class)
internal fun writeFile(inPath: String, inBytes: ByteArray) {
val vFile = fopen(inPath, "wb") ?: run {
println("fopen failed for $inPath")
return
}
fwrite(inBytes.refTo(0), 1u, inBytes.size.toULong(), vFile)
fclose(vFile)
}
/** Minimal BMP writer: 24-bit BGR (drop alpha) with the classic 40-byte
BITMAPINFOHEADER. Universally readable, including `sips`. Rows are
bottom-up (positive height) and padded to 4 bytes. */
internal fun encodeBmpBgra32(inWidth: Int, inHeight: Int, inBgra: ByteArray): ByteArray {
val kFileHeader = 14
val kInfoHeader = 40
val vRowBytes = inWidth * 3
val vRowPad = (4 - vRowBytes % 4) % 4
val vStride = vRowBytes + vRowPad
val vPixelBytes = vStride * inHeight
val vTotal = kFileHeader + kInfoHeader + vPixelBytes
val vOut = ByteArray(vTotal)
fun putU16LE(off: Int, v: Int) {
vOut[off] = (v and 0xFF).toByte()
vOut[off + 1] = ((v ushr 8) and 0xFF).toByte()
}
fun putU32LE(off: Int, v: Int) {
vOut[off] = (v and 0xFF).toByte()
vOut[off + 1] = ((v ushr 8) and 0xFF).toByte()
vOut[off + 2] = ((v ushr 16) and 0xFF).toByte()
vOut[off + 3] = ((v ushr 24) and 0xFF).toByte()
}
// BITMAPFILEHEADER
vOut[0] = 'B'.code.toByte(); vOut[1] = 'M'.code.toByte()
putU32LE(2, vTotal)
putU32LE(10, kFileHeader + kInfoHeader)
// BITMAPINFOHEADER
val info = kFileHeader
putU32LE(info + 0, kInfoHeader)
putU32LE(info + 4, inWidth)
putU32LE(info + 8, inHeight) // positive → bottom-up
putU16LE(info + 12, 1)
putU16LE(info + 14, 24)
putU32LE(info + 16, 0) // BI_RGB
putU32LE(info + 20, vPixelBytes)
putU32LE(info + 24, 2835)
putU32LE(info + 28, 2835)
putU32LE(info + 32, 0)
putU32LE(info + 36, 0)
// Source is top-down BGRA; convert to bottom-up 24-bit BGR.
val vPixelOffset = kFileHeader + kInfoHeader
for (y in 0 until inHeight) {
val vSrcRow = (inHeight - 1 - y) * inWidth * 4
val vDstRow = vPixelOffset + y * vStride
for (x in 0 until inWidth) {
vOut[vDstRow + x * 3 + 0] = inBgra[vSrcRow + x * 4 + 0] // B
vOut[vDstRow + x * 3 + 1] = inBgra[vSrcRow + x * 4 + 1] // G
vOut[vDstRow + x * 3 + 2] = inBgra[vSrcRow + x * 4 + 2] // R
}
}
return vOut
}
+113 -49
View File
@@ -12,7 +12,7 @@ plugins {
apiValidation { apiValidation {
@OptIn(ExperimentalBCVApi::class) @OptIn(ExperimentalBCVApi::class)
klib { enabled = true } klib { enabled = true }
// demo/apidemo: apps, no published API. sdl-core: naked SDL cinterop — its // demo/apidemo: apps, no published API. sdl-core: naked SDL cinterop - its
// "API" is the generated sdl3.* bindings (per-OS, can't infer macos), not our // "API" is the generated sdl3.* bindings (per-OS, can't infer macos), not our
// surface. material-symbols: auto-generated icon codepoint maps + its jvm // surface. material-symbols: auto-generated icon codepoint maps + its jvm
// dump trips BCV's ASM on newer JDKs (class file major 69). Track the // dump trips BCV's ASM on newer JDKs (class file major 69). Track the
@@ -32,27 +32,50 @@ val vPublishVersion = (System.getenv("PUBLISH_VERSION") ?: "0.0.0-SNAPSHOT").rem
// headline app-shell artifact, published under com.bitsycore.compose. // headline app-shell artifact, published under com.bitsycore.compose.
val kDefaultGroup = "com.bitsycore.compose.sdl" val kDefaultGroup = "com.bitsycore.compose.sdl"
val kAreaGroups = mapOf( val kAreaGroups = mapOf(
":ui" to "com.bitsycore.compose.ui", ":compose:ui:ui" to "com.bitsycore.compose.ui",
":ui-graphics" to "com.bitsycore.compose.ui", ":compose:ui:ui-graphics" to "com.bitsycore.compose.ui",
":ui-text" to "com.bitsycore.compose.ui", ":compose:ui:ui-text" to "com.bitsycore.compose.ui",
":ui-unit" to "com.bitsycore.compose.ui", ":compose:ui:ui-unit" to "com.bitsycore.compose.ui",
":ui-geometry" to "com.bitsycore.compose.ui", ":compose:ui:ui-geometry" to "com.bitsycore.compose.ui",
":ui-util" to "com.bitsycore.compose.ui", ":compose:ui:ui-util" to "com.bitsycore.compose.ui",
":ui-backhandler" to "com.bitsycore.compose.ui", ":compose:ui:ui-backhandler" to "com.bitsycore.compose.ui",
":ui-tooling-preview" to "com.bitsycore.compose.ui", ":compose:ui:ui-tooling-preview" to "com.bitsycore.compose.ui",
":foundation" to "com.bitsycore.compose.foundation", ":compose:foundation:foundation" to "com.bitsycore.compose.foundation",
":foundation-layout" to "com.bitsycore.compose.foundation", ":compose:foundation:foundation-layout" to "com.bitsycore.compose.foundation",
":animation" to "com.bitsycore.compose.animation", ":compose:animation:animation" to "com.bitsycore.compose.animation",
":animation-core" to "com.bitsycore.compose.animation", ":compose:animation:animation-core" to "com.bitsycore.compose.animation",
":animation-graphics" to "com.bitsycore.compose.animation", ":compose:animation:animation-graphics" to "com.bitsycore.compose.animation",
":material3" to "com.bitsycore.compose.material3", ":compose:material3:material3" to "com.bitsycore.compose.material3",
":material-ripple" to "com.bitsycore.compose.material", ":compose:material3:adaptive:adaptive" to "com.bitsycore.compose.material3.adaptive",
":components-resources" to "com.bitsycore.compose.components", ":compose:material3:adaptive:adaptive-layout" to "com.bitsycore.compose.material3.adaptive",
":navigation3-ui" to "com.bitsycore.navigation3", ":compose:material3:adaptive:adaptive-navigation" to "com.bitsycore.compose.material3.adaptive",
":desktop-native-window" to "com.bitsycore.compose", ":compose:material3:adaptive:adaptive-navigation3" to "com.bitsycore.compose.material3.adaptive",
":compose:material:material-ripple" to "com.bitsycore.compose.material",
":components:resources:components-resources" to "com.bitsycore.compose.components",
":navigation3:navigation3-ui" to "com.bitsycore.navigation3",
":koin:koin-core-viewmodel" to "com.bitsycore.koin",
":koin:koin-compose" to "com.bitsycore.koin",
":koin:koin-compose-viewmodel" to "com.bitsycore.koin",
":koin:koin-compose-navigation3" to "com.bitsycore.koin",
":coil:coil-core" to "com.bitsycore.coil3",
":coil:coil" to "com.bitsycore.coil3",
":coil:coil-compose-core" to "com.bitsycore.coil3",
":coil:coil-compose" to "com.bitsycore.coil3",
":coil:coil-svg" to "com.bitsycore.coil3",
":coil:coil-network-core" to "com.bitsycore.coil3",
":coil:coil-network-ktor3" to "com.bitsycore.coil3",
":pulse:pulse" to "com.bitsycore.compose.desktop.native.pulse",
":pulse:pulse-viewmodel" to "com.bitsycore.compose.desktop.native.pulse",
":pulse:pulse-savedstate" to "com.bitsycore.compose.desktop.native.pulse",
":pulse:pulse-compose" to "com.bitsycore.compose.desktop.native.pulse",
":compose:desktop:native:desktop-native-window" to "com.bitsycore.compose",
) )
fun groupFor(path: String): String = kAreaGroups[path] ?: kDefaultGroup fun groupFor(path: String): String = kAreaGroups[path] ?: kDefaultGroup
// The published artifactId is always the leaf project name - settings.gradle.kts
// keeps the leaf equal to the artifactId even where the directory differs.
fun artifactIdFor(path: String): String = path.substringAfterLast(':')
allprojects { allprojects {
group = groupFor(path) group = groupFor(path)
version = vPublishVersion version = vPublishVersion
@@ -63,7 +86,7 @@ allprojects {
// ================== // ==================
// Every library module (everything except the two demo apps) auto-registers a // Every library module (everything except the two demo apps) auto-registers a
// MavenPublication via the kotlin-multiplatform plugin — one per target + one // MavenPublication via the kotlin-multiplatform plugin - one per target + one
// for the shared kotlinMultiplatform metadata. The CI publish workflow runs on // for the shared kotlinMultiplatform metadata. The CI publish workflow runs on
// three hosts (macOS / Linux / Windows) and each invokes only the publication // three hosts (macOS / Linux / Windows) and each invokes only the publication
// tasks Gradle actually generated for its own targets, so the group of hosts // tasks Gradle actually generated for its own targets, so the group of hosts
@@ -72,19 +95,27 @@ allprojects {
val kAppModules = setOf(":demo", ":apidemo") val kAppModules = setOf(":demo", ":apidemo")
val kPublishedLibs = setOf( val kPublishedLibs = setOf(
":sdl-core", ":sdl:sdl-core",
":ui", ":ui-util", ":ui-geometry", ":ui-graphics", ":ui-text", ":compose:ui:ui", ":compose:ui:ui-util", ":compose:ui:ui-geometry",
":ui-unit", ":ui-backhandler", ":ui-tooling-preview", ":compose:ui:ui-graphics", ":compose:ui:ui-text",
":animation-core", ":animation", ":animation-graphics", ":compose:ui:ui-unit", ":compose:ui:ui-backhandler", ":compose:ui:ui-tooling-preview",
":foundation", ":foundation-layout", ":compose:animation:animation-core", ":compose:animation:animation",
":material3", ":material-ripple", ":compose:animation:animation-graphics",
":desktop-native-window", ":material-symbols", ":compose:foundation:foundation", ":compose:foundation:foundation-layout",
":navigation3-ui", ":components-resources", ":compose:material3:material3", ":compose:material:material-ripple",
":compose:material3:adaptive:adaptive", ":compose:material3:adaptive:adaptive-layout", ":compose:material3:adaptive:adaptive-navigation", ":compose:material3:adaptive:adaptive-navigation3",
":compose:desktop:native:desktop-native-window", ":utils:material-symbols",
":navigation3:navigation3-ui", ":components:resources:components-resources",
":koin:koin-core-viewmodel", ":koin:koin-compose",
":koin:koin-compose-viewmodel", ":koin:koin-compose-navigation3",
":coil:coil-core", ":coil:coil", ":coil:coil-compose-core", ":coil:coil-compose",
":coil:coil-svg", ":coil:coil-network-core", ":coil:coil-network-ktor3",
":pulse:pulse", ":pulse:pulse-viewmodel", ":pulse:pulse-savedstate", ":pulse:pulse-compose",
) )
// -PuseGithubPackages=true swaps every `project(":<lib>")` reference the demo // -PuseGithubPackages=true swaps every `project(":<lib>")` reference the demo
// apps make for the published Maven coordinate. Library modules keep resolving // apps make for the published Maven coordinate. Library modules keep resolving
// each other as `project(...)` — the substitution only fires at the // each other as `project(...)` - the substitution only fires at the
// app→library boundary, so the swap validates end-to-end consumption of the // app→library boundary, so the swap validates end-to-end consumption of the
// published klibs without touching the source of `implementation(project(...))`. // published klibs without touching the source of `implementation(project(...))`.
// Version defaults to 0.1.0 (matches the git tag) but can be pinned via -PconsumeVersion=…. // Version defaults to 0.1.0 (matches the git tag) but can be pinned via -PconsumeVersion=….
@@ -110,7 +141,7 @@ subprojects {
configurations.configureEach { configurations.configureEach {
resolutionStrategy.dependencySubstitution { resolutionStrategy.dependencySubstitution {
kPublishedLibs.forEach { modulePath -> kPublishedLibs.forEach { modulePath ->
val vArtifactId = modulePath.removePrefix(":") val vArtifactId = artifactIdFor(modulePath)
substitute(project(modulePath)) substitute(project(modulePath))
.using(module("${groupFor(modulePath)}:$vArtifactId:$kConsumeVersion")) .using(module("${groupFor(modulePath)}:$vArtifactId:$kConsumeVersion"))
.because("-PuseGithubPackages=true") .because("-PuseGithubPackages=true")
@@ -128,6 +159,9 @@ subprojects {
} }
return@subprojects return@subprojects
} }
// Nesting the paths under compose/ / sdl/ / utils/ … materialises container
// projects (:compose, :compose:ui, …) that hold no code - skip them.
if (path !in kPublishedLibs) return@subprojects
plugins.apply("maven-publish") plugins.apply("maven-publish")
afterEvaluate { afterEvaluate {
extensions.configure<PublishingExtension> { extensions.configure<PublishingExtension> {
@@ -153,7 +187,7 @@ subprojects {
publications.withType<MavenPublication>().configureEach { publications.withType<MavenPublication>().configureEach {
pom { pom {
name.set("${rootProject.name} ${project.name}") name.set("${rootProject.name} ${project.name}")
description.set("Compose Multiplatform on SDL3 (Kotlin/Native, no JVM) — ${project.name}") description.set("Compose Multiplatform on SDL3 (Kotlin/Native, no JVM) - ${project.name}")
url.set("https://github.com/${System.getenv("GITHUB_REPOSITORY") ?: "bitsycore/ComposeDesktopNative"}") url.set("https://github.com/${System.getenv("GITHUB_REPOSITORY") ?: "bitsycore/ComposeDesktopNative"}")
licenses { licenses {
license { license {
@@ -170,7 +204,7 @@ subprojects {
// FULL-COMMONIZATION BRIDGE (repo-wide): a module may declare the OFFICIAL // FULL-COMMONIZATION BRIDGE (repo-wide): a module may declare the OFFICIAL
// Maven Compose artifacts in its commonMain so metadata + jvm resolve them // Maven Compose artifacts in its commonMain so metadata + jvm resolve them
// (e.g. :material-symbols' common API); every NATIVE target configuration // (e.g. :material-symbols' common API); every NATIVE target configuration
// swaps those modules for the port's project equivalents — the Maven // swaps those modules for the port's project equivalents - the Maven
// artifacts ship no mingwX64/linux klibs. org.jetbrains.compose.runtime is // artifacts ship no mingwX64/linux klibs. org.jetbrains.compose.runtime is
// deliberately NOT here: the port uses the official runtime klibs everywhere. // deliberately NOT here: the port uses the official runtime klibs everywhere.
val vNativeTargetTokens = listOf("mingwX64", "linuxX64", "linuxArm64", "macosArm64") val vNativeTargetTokens = listOf("mingwX64", "linuxX64", "linuxArm64", "macosArm64")
@@ -178,34 +212,64 @@ allprojects {
configurations.configureEach { configurations.configureEach {
if (vNativeTargetTokens.any { name.contains(it, ignoreCase = true) }) { if (vNativeTargetTokens.any { name.contains(it, ignoreCase = true) }) {
resolutionStrategy.dependencySubstitution { resolutionStrategy.dependencySubstitution {
substitute(module("org.jetbrains.compose.ui:ui")).using(project(":ui")) substitute(module("org.jetbrains.compose.ui:ui")).using(project(":compose:ui:ui"))
// ui-graphics / ui-text are their own modules (split out of :ui, // ui-graphics / ui-text are their own modules (split out of :ui,
// upstream layout). Declare each DIRECTLY so the app commonMain sees // upstream layout). Declare each DIRECTLY so the app commonMain sees
// them under the granular-metadata visibility rule. // them under the granular-metadata visibility rule.
substitute(module("org.jetbrains.compose.ui:ui-graphics")).using(project(":ui-graphics")) substitute(module("org.jetbrains.compose.ui:ui-graphics")).using(project(":compose:ui:ui-graphics"))
substitute(module("org.jetbrains.compose.ui:ui-text")).using(project(":ui-text")) substitute(module("org.jetbrains.compose.ui:ui-text")).using(project(":compose:ui:ui-text"))
substitute(module("org.jetbrains.compose.ui:ui-unit")).using(project(":ui-unit")) substitute(module("org.jetbrains.compose.ui:ui-unit")).using(project(":compose:ui:ui-unit"))
substitute(module("org.jetbrains.compose.ui:ui-geometry")).using(project(":ui-geometry")) substitute(module("org.jetbrains.compose.ui:ui-geometry")).using(project(":compose:ui:ui-geometry"))
substitute(module("org.jetbrains.compose.ui:ui-util")).using(project(":ui-util")) substitute(module("org.jetbrains.compose.ui:ui-util")).using(project(":compose:ui:ui-util"))
substitute(module("org.jetbrains.compose.ui:ui-tooling-preview")).using(project(":ui-tooling-preview")) substitute(module("org.jetbrains.compose.ui:ui-tooling-preview")).using(project(":compose:ui:ui-tooling-preview"))
substitute(module("org.jetbrains.compose.foundation:foundation")).using(project(":foundation")) substitute(module("org.jetbrains.compose.foundation:foundation")).using(project(":compose:foundation:foundation"))
substitute(module("org.jetbrains.compose.foundation:foundation-layout")).using(project(":foundation-layout")) substitute(module("org.jetbrains.compose.foundation:foundation-layout")).using(project(":compose:foundation:foundation-layout"))
substitute(module("org.jetbrains.compose.animation:animation")).using(project(":animation")) substitute(module("org.jetbrains.compose.animation:animation")).using(project(":compose:animation:animation"))
substitute(module("org.jetbrains.compose.animation:animation-core")).using(project(":animation-core")) substitute(module("org.jetbrains.compose.animation:animation-core")).using(project(":compose:animation:animation-core"))
substitute(module("org.jetbrains.compose.material3:material3")).using(project(":material3")) substitute(module("org.jetbrains.compose.material3:material3")).using(project(":compose:material3:material3"))
// material3-adaptive: upstream publishes ios + macosArm64 only - no
// linux, no mingw (issues/4). Vendored as :compose:material3:adaptive:*.
substitute(module("org.jetbrains.compose.material3.adaptive:adaptive")).using(project(":compose:material3:adaptive:adaptive"))
substitute(module("org.jetbrains.compose.material3.adaptive:adaptive-layout")).using(project(":compose:material3:adaptive:adaptive-layout"))
substitute(module("org.jetbrains.compose.material3.adaptive:adaptive-navigation")).using(project(":compose:material3:adaptive:adaptive-navigation"))
substitute(module("org.jetbrains.compose.material3.adaptive:adaptive-navigation3")).using(project(":compose:material3:adaptive:adaptive-navigation3"))
// navigation3-ui: the JB Maven artifact has no K/N desktop // navigation3-ui: the JB Maven artifact has no K/N desktop
// klibs — the port vendors it as :navigation3-ui. // klibs - the port vendors it as :navigation3-ui.
substitute(module("org.jetbrains.androidx.navigation3:navigation3-ui")).using(project(":navigation3-ui")) substitute(module("org.jetbrains.androidx.navigation3:navigation3-ui")).using(project(":navigation3:navigation3-ui"))
// components-resources: the official resources runtime ships no // components-resources: the official resources runtime ships no
// mingwX64/linux klibs — the port vendors it as :components-resources. // mingwX64/linux klibs - the port vendors it as :components-resources.
substitute(module("org.jetbrains.compose.components:components-resources")).using(project(":components-resources")) substitute(module("org.jetbrains.compose.components:components-resources")).using(project(":components:resources:components-resources"))
// Koin: koin-core itself publishes mingwX64 + linux and is used
// from Maven; these four stop at apple+android upstream.
substitute(module("io.insert-koin:koin-core-viewmodel")).using(project(":koin:koin-core-viewmodel"))
substitute(module("io.insert-koin:koin-compose")).using(project(":koin:koin-compose"))
substitute(module("io.insert-koin:koin-compose-viewmodel")).using(project(":koin:koin-compose-viewmodel"))
substitute(module("io.insert-koin:koin-compose-navigation3")).using(project(":koin:koin-compose-navigation3"))
// Coil 3: no mingwX64 upstream anywhere, and no linux for the
// compose layer either.
substitute(module("io.coil-kt.coil3:coil-core")).using(project(":coil:coil-core"))
substitute(module("io.coil-kt.coil3:coil")).using(project(":coil:coil"))
substitute(module("io.coil-kt.coil3:coil-compose-core")).using(project(":coil:coil-compose-core"))
substitute(module("io.coil-kt.coil3:coil-compose")).using(project(":coil:coil-compose"))
substitute(module("io.coil-kt.coil3:coil-svg")).using(project(":coil:coil-svg"))
substitute(module("io.coil-kt.coil3:coil-network-core")).using(project(":coil:coil-network-core"))
substitute(module("io.coil-kt.coil3:coil-network-ktor3")).using(project(":coil:coil-network-ktor3"))
// Pulse MVI: upstream publishes com.bitsycore.lib:* with no desktop
// native targets. Pulse is itself a com.bitsycore library, so the port's
// republish is namespaced under THIS project rather than a bare
// com.bitsycore.pulse, which would read like the upstream artifact. The
// two coordinates never collide, and substitutes the official one here.
substitute(module("com.bitsycore.lib:pulse")).using(project(":pulse:pulse"))
substitute(module("com.bitsycore.lib:pulse-viewmodel")).using(project(":pulse:pulse-viewmodel"))
substitute(module("com.bitsycore.lib:pulse-savedstate")).using(project(":pulse:pulse-savedstate"))
substitute(module("com.bitsycore.lib:pulse-compose")).using(project(":pulse:pulse-compose"))
} }
} }
} }
} }
// Whether the current host can build the mingwX64 target. Kotlin/Native can // Whether the current host can build the mingwX64 target. Kotlin/Native can
// only cross-compile the mingwX64 sdl3 cinterop from a Windows host — it needs // only cross-compile the mingwX64 sdl3 cinterop from a Windows host - it needs
// the Windows SDL3 headers under libs/ (produced by scripts/build-sdl/build-all.py // the Windows SDL3 headers under libs/ (produced by scripts/build-sdl/build-all.py
// run on a Windows host). Declaring `mingwX64()` on a non-Windows host is safe // run on a Windows host). Declaring `mingwX64()` on a non-Windows host is safe
// for pure-Kotlin modules but blows up the moment the sdl3 cinterop tries to // for pure-Kotlin modules but blows up the moment the sdl3 cinterop tries to
@@ -20,7 +20,7 @@ import java.net.URI
// ================== // ==================
/** /**
* Per-app knobs for [registerComposeFontBundling]. EVERYTHING is opt-in — with no * Per-app knobs for [registerComposeFontBundling]. EVERYTHING is opt-in - with no
* flag set the call registers no tasks and stages nothing. * flag set the call registers no tasks and stages nothing.
*/ */
class ComposeFontBundlingConfig { class ComposeFontBundlingConfig {
@@ -31,7 +31,7 @@ class ComposeFontBundlingConfig {
var bundleNotoSans: Boolean = false var bundleNotoSans: Boolean = false
/** /**
* Always bundle font/NotoSansMono.ttf — for apps that load the mono font through * Always bundle font/NotoSansMono.ttf - for apps that load the mono font through
* their own seam (apidemo's body font, Fonts.kt), invisible to auto-detection. * their own seam (apidemo's body font, Fonts.kt), invisible to auto-detection.
*/ */
var bundleNotoSansMono: Boolean = false var bundleNotoSansMono: Boolean = false
@@ -51,7 +51,7 @@ class ComposeFontBundlingConfig {
/** /**
* Subset each bundled Material Symbols font to the glyphs the sources reference * Subset each bundled Material Symbols font to the glyphs the sources reference
* (scripts/subset-material-symbols.py picks the codepoints; the subset task uses * (scripts/subset-material-symbols.py picks the codepoints; the subset task uses
* hb-subset when present, else fontTools via Python — auto-installed — and only * hb-subset when present, else fontTools via Python - auto-installed - and only
* bundles the full font if neither is available). Needs [bundleMaterialSymbols]; * bundles the full font if neither is available). Needs [bundleMaterialSymbols];
* still gated by -PsubsetIcons. * still gated by -PsubsetIcons.
*/ */
@@ -74,11 +74,11 @@ private val kMonospaceRegex = Regex("\\bFontFamily\\.Monospace\\b")
/** /**
* Wires the app font pipeline shared by :demo and :apidemo. Everything is OPT-IN via * Wires the app font pipeline shared by :demo and :apidemo. Everything is OPT-IN via
* [ComposeFontBundlingConfig] — a bare call is a no-op. * [ComposeFontBundlingConfig] - a bare call is a no-op.
* *
* - Registers downloadNotoFonts (Noto Sans + Noto Sans Mono variable fonts → * - Registers downloadNotoFonts (Noto Sans + Noto Sans Mono variable fonts →
* build/fonts/) when a Noto font is bundled. * build/fonts/) when a Noto font is bundled.
* - Adds the opted-in font/ entries to every data.kres Zip task — the bridge plugin's * - Adds the opted-in font/ entries to every data.kres Zip task - the bridge plugin's
* package<Variant>ComposeResources<Target> tasks or an app's own copy* tasks * package<Variant>ComposeResources<Target> tasks or an app's own copy* tasks
* (matching{} is lazy: the plugin's tasks appear in its afterEvaluate). Material * (matching{} is lazy: the plugin's tasks appear in its afterEvaluate). Material
* Symbols fonts are hb-subset to the used glyphs when subsetting is enabled. * Symbols fonts are hb-subset to the used glyphs when subsetting is enabled.
@@ -142,7 +142,7 @@ fun Project.registerComposeFontBundling(configure: ComposeFontBundlingConfig.()
addFontEntries(this, vNotoFiles, vDownloadNotoFonts, vUsedStyles, vSubsetTasksByStyle) addFontEntries(this, vNotoFiles, vDownloadNotoFonts, vUsedStyles, vSubsetTasksByStyle)
} }
tasks.withType<ProcessResources>().matching { it.name == "jvmProcessResources" }.configureEach { tasks.withType<ProcessResources>().matching { it.name == "jvmProcessResources" }.configureEach {
// Full fonts on JVM — Skiko applies the variable axes itself (Typeface.makeClone). // Full fonts on JVM - Skiko applies the variable axes itself (Typeface.makeClone).
addFontEntries(this, vNotoFiles, vDownloadNotoFonts, vUsedStyles, emptyMap()) addFontEntries(this, vNotoFiles, vDownloadNotoFonts, vUsedStyles, emptyMap())
} }
} }
@@ -182,7 +182,7 @@ private fun Project.detectUsedStyles(): List<String> {
// ================== // ==================
/** /**
* Adds the font/ entries (Notos + used Material Symbols styles) to inTask — a data.kres * Adds the font/ entries (Notos + used Material Symbols styles) to inTask - a data.kres
* Zip or jvmProcessResources. inSubsetTasksByStyle maps styles to hb-subset tasks whose * Zip or jvmProcessResources. inSubsetTasksByStyle maps styles to hb-subset tasks whose
* output replaces the full font; pass an empty map to stage the full fonts. * output replaces the full font; pass an empty map to stage the full fonts.
*/ */
@@ -197,7 +197,7 @@ private fun Project.addFontEntries(
inTask.from(vNotoFile) { into("font") } inTask.from(vNotoFile) { into("font") }
inDownloadNotoFonts?.let { inTask.dependsOn(it) } inDownloadNotoFonts?.let { inTask.dependsOn(it) }
} }
val vSymbolsProject = rootProject.project(":material-symbols") val vSymbolsProject = rootProject.project(":utils:material-symbols")
for (vStyle in inUsedStyles) { for (vStyle in inUsedStyles) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val vFontFile = vSymbolsProject.extra["iconFontFile$vStyle"] as Provider<RegularFile> val vFontFile = vSymbolsProject.extra["iconFontFile$vStyle"] as Provider<RegularFile>
@@ -232,7 +232,7 @@ private fun Project.registerIconSubsetPipeline(inUsedStyles: List<String>): Map<
val vFindUsage = tasks.register<Exec>("findMaterialSymbolsUsage") { val vFindUsage = tasks.register<Exec>("findMaterialSymbolsUsage") {
description = "Scan src/ for MaterialSymbols.<Name> usages → usage-codepoint.txt." description = "Scan src/ for MaterialSymbols.<Name> usages → usage-codepoint.txt."
val vScript = rootProject.layout.projectDirectory.file("scripts/subset-material-symbols.py").asFile val vScript = rootProject.layout.projectDirectory.file("scripts/subset-material-symbols.py").asFile
val vConstants = rootProject.project(":material-symbols").layout.projectDirectory val vConstants = rootProject.project(":utils:material-symbols").layout.projectDirectory
.file("src/commonMain/kotlin/com/compose/sdl/icons/MaterialSymbols.kt").asFile .file("src/commonMain/kotlin/com/compose/sdl/icons/MaterialSymbols.kt").asFile
val vUsageFile = vIconsBuildDir.get().file("usage-codepoint.txt").asFile val vUsageFile = vIconsBuildDir.get().file("usage-codepoint.txt").asFile
inputs.files(fileTree("src") { include("**/*.kt") }) inputs.files(fileTree("src") { include("**/*.kt") })
@@ -259,7 +259,7 @@ private fun Project.registerSubsetTask(
inIconsBuildDir: Provider<Directory>, inIconsBuildDir: Provider<Directory>,
inFindUsage: TaskProvider<*>, inFindUsage: TaskProvider<*>,
): TaskProvider<*> { ): TaskProvider<*> {
val vSymbolsProject = rootProject.project(":material-symbols") val vSymbolsProject = rootProject.project(":utils:material-symbols")
return tasks.register("subsetMaterialSymbols$inStyle") { return tasks.register("subsetMaterialSymbols$inStyle") {
description = "hb-subset the $inStyle Material Symbols font to icons actually used." description = "hb-subset the $inStyle Material Symbols font to icons actually used."
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -304,21 +304,21 @@ private fun Project.registerSubsetTask(
) )
} }
// 1. hb-subset (harfbuzz) — fastest path when it is on PATH. // 1. hb-subset (harfbuzz) - fastest path when it is on PATH.
if (runCmd("hb-subset", vInputFile.absolutePath, "-o", vOut.absolutePath, "--unicodes=$vUnicodes").first if (runCmd("hb-subset", vInputFile.absolutePath, "-o", vOut.absolutePath, "--unicodes=$vUnicodes").first
&& vOut.length() > 0L && vOut.length() > 0L
) { ) {
logShrink("hb-subset"); return@doLast logShrink("hb-subset"); return@doLast
} }
// 2. fontTools (pyftsubset) via Python — the portable fallback. Python is // 2. fontTools (pyftsubset) via Python - the portable fallback. Python is
// already a build prerequisite (SDL build + vendor sync) and fontTools is // already a build prerequisite (SDL build + vendor sync) and fontTools is
// variable-font-aware, so the FILL/wght/GRAD/opsz axes survive. It is // variable-font-aware, so the FILL/wght/GRAD/opsz axes survive. It is
// auto-installed if missing, so no manual harfbuzz install is ever required. // auto-installed if missing, so no manual harfbuzz install is ever required.
val vPython = listOf("python3", "python", "py").firstOrNull { runCmd(it, "--version").first } val vPython = listOf("python3", "python", "py").firstOrNull { runCmd(it, "--version").first }
if (vPython != null) { if (vPython != null) {
if (!runCmd(vPython, "-c", "import fontTools").first) { if (!runCmd(vPython, "-c", "import fontTools").first) {
logger.lifecycle("[subset $inStyle] fontTools not present — installing it via pip…") logger.lifecycle("[subset $inStyle] fontTools not present - installing it via pip…")
// --user (no root) → --break-system-packages (PEP 668) → plain. // --user (no root) → --break-system-packages (PEP 668) → plain.
listOf(listOf("--user"), listOf("--break-system-packages"), emptyList<String>()) listOf(listOf("--user"), listOf("--break-system-packages"), emptyList<String>())
.firstOrNull { vExtra -> .firstOrNull { vExtra ->
@@ -336,7 +336,7 @@ private fun Project.registerSubsetTask(
logger.warn("[subset $inStyle] fontTools subset failed :\n$vFtOut") logger.warn("[subset $inStyle] fontTools subset failed :\n$vFtOut")
} }
// 3. Last resort : the full font — correct, just larger. // 3. Last resort : the full font - correct, just larger.
vInputFile.copyTo(vOut, overwrite = true) vInputFile.copyTo(vOut, overwrite = true)
logger.warn( logger.warn(
"[subset $inStyle] no working subsetter found (hb-subset or python3+fontTools) : " + "[subset $inStyle] no working subsetter found (hb-subset or python3+fontTools) : " +
@@ -0,0 +1,240 @@
// Klib ABI Dump
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.coil3:coil-compose-core>
abstract fun interface coil3.compose/AsyncImagePreviewHandler { // coil3.compose/AsyncImagePreviewHandler|null[0]
abstract suspend fun handle(coil3/ImageLoader, coil3.request/ImageRequest): coil3.compose/AsyncImagePainter.State // coil3.compose/AsyncImagePreviewHandler.handle|handle(coil3.ImageLoader;coil3.request.ImageRequest){}[0]
final object Companion { // coil3.compose/AsyncImagePreviewHandler.Companion|null[0]
final val Default // coil3.compose/AsyncImagePreviewHandler.Companion.Default|{}Default[0]
final fun <get-Default>(): coil3.compose/AsyncImagePreviewHandler // coil3.compose/AsyncImagePreviewHandler.Companion.Default.<get-Default>|<get-Default>(){}[0]
}
}
abstract interface coil3.compose/AsyncImageModelEqualityDelegate { // coil3.compose/AsyncImageModelEqualityDelegate|null[0]
abstract fun equals(kotlin/Any?, kotlin/Any?): kotlin/Boolean // coil3.compose/AsyncImageModelEqualityDelegate.equals|equals(kotlin.Any?;kotlin.Any?){}[0]
abstract fun hashCode(kotlin/Any?): kotlin/Int // coil3.compose/AsyncImageModelEqualityDelegate.hashCode|hashCode(kotlin.Any?){}[0]
final object Companion { // coil3.compose/AsyncImageModelEqualityDelegate.Companion|null[0]
final val AllProperties // coil3.compose/AsyncImageModelEqualityDelegate.Companion.AllProperties|{}AllProperties[0]
final fun <get-AllProperties>(): coil3.compose/AsyncImageModelEqualityDelegate // coil3.compose/AsyncImageModelEqualityDelegate.Companion.AllProperties.<get-AllProperties>|<get-AllProperties>(){}[0]
final val Default // coil3.compose/AsyncImageModelEqualityDelegate.Companion.Default|{}Default[0]
final fun <get-Default>(): coil3.compose/AsyncImageModelEqualityDelegate // coil3.compose/AsyncImageModelEqualityDelegate.Companion.Default.<get-Default>|<get-Default>(){}[0]
}
}
abstract interface coil3.compose/DrawScopeSizeResolver : coil3.size/SizeResolver { // coil3.compose/DrawScopeSizeResolver|null[0]
abstract fun connect(kotlinx.coroutines.flow/Flow<androidx.compose.ui.geometry/Size>) // coil3.compose/DrawScopeSizeResolver.connect|connect(kotlinx.coroutines.flow.Flow<androidx.compose.ui.geometry.Size>){}[0]
}
abstract interface coil3.compose/SubcomposeAsyncImageScope : androidx.compose.foundation.layout/BoxScope { // coil3.compose/SubcomposeAsyncImageScope|null[0]
abstract val alignment // coil3.compose/SubcomposeAsyncImageScope.alignment|{}alignment[0]
abstract fun <get-alignment>(): androidx.compose.ui/Alignment // coil3.compose/SubcomposeAsyncImageScope.alignment.<get-alignment>|<get-alignment>(){}[0]
abstract val alpha // coil3.compose/SubcomposeAsyncImageScope.alpha|{}alpha[0]
abstract fun <get-alpha>(): kotlin/Float // coil3.compose/SubcomposeAsyncImageScope.alpha.<get-alpha>|<get-alpha>(){}[0]
abstract val clipToBounds // coil3.compose/SubcomposeAsyncImageScope.clipToBounds|{}clipToBounds[0]
abstract fun <get-clipToBounds>(): kotlin/Boolean // coil3.compose/SubcomposeAsyncImageScope.clipToBounds.<get-clipToBounds>|<get-clipToBounds>(){}[0]
abstract val colorFilter // coil3.compose/SubcomposeAsyncImageScope.colorFilter|{}colorFilter[0]
abstract fun <get-colorFilter>(): androidx.compose.ui.graphics/ColorFilter? // coil3.compose/SubcomposeAsyncImageScope.colorFilter.<get-colorFilter>|<get-colorFilter>(){}[0]
abstract val contentDescription // coil3.compose/SubcomposeAsyncImageScope.contentDescription|{}contentDescription[0]
abstract fun <get-contentDescription>(): kotlin/String? // coil3.compose/SubcomposeAsyncImageScope.contentDescription.<get-contentDescription>|<get-contentDescription>(){}[0]
abstract val contentScale // coil3.compose/SubcomposeAsyncImageScope.contentScale|{}contentScale[0]
abstract fun <get-contentScale>(): androidx.compose.ui.layout/ContentScale // coil3.compose/SubcomposeAsyncImageScope.contentScale.<get-contentScale>|<get-contentScale>(){}[0]
abstract val painter // coil3.compose/SubcomposeAsyncImageScope.painter|{}painter[0]
abstract fun <get-painter>(): coil3.compose/AsyncImagePainter // coil3.compose/SubcomposeAsyncImageScope.painter.<get-painter>|<get-painter>(){}[0]
}
final class coil3.compose/AsyncImagePainter : androidx.compose.runtime/RememberObserver, androidx.compose.ui.graphics.painter/Painter { // coil3.compose/AsyncImagePainter|null[0]
final val input // coil3.compose/AsyncImagePainter.input|{}input[0]
final fun <get-input>(): kotlinx.coroutines.flow/StateFlow<coil3.compose/AsyncImagePainter.Input> // coil3.compose/AsyncImagePainter.input.<get-input>|<get-input>(){}[0]
final val intrinsicSize // coil3.compose/AsyncImagePainter.intrinsicSize|{}intrinsicSize[0]
final fun <get-intrinsicSize>(): androidx.compose.ui.geometry/Size // coil3.compose/AsyncImagePainter.intrinsicSize.<get-intrinsicSize>|<get-intrinsicSize>(){}[0]
final val state // coil3.compose/AsyncImagePainter.state|{}state[0]
final fun <get-state>(): kotlinx.coroutines.flow/StateFlow<coil3.compose/AsyncImagePainter.State> // coil3.compose/AsyncImagePainter.state.<get-state>|<get-state>(){}[0]
final fun onAbandoned() // coil3.compose/AsyncImagePainter.onAbandoned|onAbandoned(){}[0]
final fun onForgotten() // coil3.compose/AsyncImagePainter.onForgotten|onForgotten(){}[0]
final fun onRemembered() // coil3.compose/AsyncImagePainter.onRemembered|onRemembered(){}[0]
final fun restart() // coil3.compose/AsyncImagePainter.restart|restart(){}[0]
sealed interface State { // coil3.compose/AsyncImagePainter.State|null[0]
abstract val painter // coil3.compose/AsyncImagePainter.State.painter|{}painter[0]
abstract fun <get-painter>(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/AsyncImagePainter.State.painter.<get-painter>|<get-painter>(){}[0]
final class Error : coil3.compose/AsyncImagePainter.State { // coil3.compose/AsyncImagePainter.State.Error|null[0]
constructor <init>(androidx.compose.ui.graphics.painter/Painter?, coil3.request/ErrorResult) // coil3.compose/AsyncImagePainter.State.Error.<init>|<init>(androidx.compose.ui.graphics.painter.Painter?;coil3.request.ErrorResult){}[0]
final val painter // coil3.compose/AsyncImagePainter.State.Error.painter|{}painter[0]
final fun <get-painter>(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/AsyncImagePainter.State.Error.painter.<get-painter>|<get-painter>(){}[0]
final val result // coil3.compose/AsyncImagePainter.State.Error.result|{}result[0]
final fun <get-result>(): coil3.request/ErrorResult // coil3.compose/AsyncImagePainter.State.Error.result.<get-result>|<get-result>(){}[0]
final fun component1(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/AsyncImagePainter.State.Error.component1|component1(){}[0]
final fun component2(): coil3.request/ErrorResult // coil3.compose/AsyncImagePainter.State.Error.component2|component2(){}[0]
final fun copy(androidx.compose.ui.graphics.painter/Painter? = ..., coil3.request/ErrorResult = ...): coil3.compose/AsyncImagePainter.State.Error // coil3.compose/AsyncImagePainter.State.Error.copy|copy(androidx.compose.ui.graphics.painter.Painter?;coil3.request.ErrorResult){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.compose/AsyncImagePainter.State.Error.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // coil3.compose/AsyncImagePainter.State.Error.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // coil3.compose/AsyncImagePainter.State.Error.toString|toString(){}[0]
}
final class Loading : coil3.compose/AsyncImagePainter.State { // coil3.compose/AsyncImagePainter.State.Loading|null[0]
constructor <init>(androidx.compose.ui.graphics.painter/Painter?) // coil3.compose/AsyncImagePainter.State.Loading.<init>|<init>(androidx.compose.ui.graphics.painter.Painter?){}[0]
final val painter // coil3.compose/AsyncImagePainter.State.Loading.painter|{}painter[0]
final fun <get-painter>(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/AsyncImagePainter.State.Loading.painter.<get-painter>|<get-painter>(){}[0]
final fun component1(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/AsyncImagePainter.State.Loading.component1|component1(){}[0]
final fun copy(androidx.compose.ui.graphics.painter/Painter? = ...): coil3.compose/AsyncImagePainter.State.Loading // coil3.compose/AsyncImagePainter.State.Loading.copy|copy(androidx.compose.ui.graphics.painter.Painter?){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.compose/AsyncImagePainter.State.Loading.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // coil3.compose/AsyncImagePainter.State.Loading.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // coil3.compose/AsyncImagePainter.State.Loading.toString|toString(){}[0]
}
final class Success : coil3.compose/AsyncImagePainter.State { // coil3.compose/AsyncImagePainter.State.Success|null[0]
constructor <init>(androidx.compose.ui.graphics.painter/Painter, coil3.request/SuccessResult) // coil3.compose/AsyncImagePainter.State.Success.<init>|<init>(androidx.compose.ui.graphics.painter.Painter;coil3.request.SuccessResult){}[0]
final val painter // coil3.compose/AsyncImagePainter.State.Success.painter|{}painter[0]
final fun <get-painter>(): androidx.compose.ui.graphics.painter/Painter // coil3.compose/AsyncImagePainter.State.Success.painter.<get-painter>|<get-painter>(){}[0]
final val result // coil3.compose/AsyncImagePainter.State.Success.result|{}result[0]
final fun <get-result>(): coil3.request/SuccessResult // coil3.compose/AsyncImagePainter.State.Success.result.<get-result>|<get-result>(){}[0]
final fun component1(): androidx.compose.ui.graphics.painter/Painter // coil3.compose/AsyncImagePainter.State.Success.component1|component1(){}[0]
final fun component2(): coil3.request/SuccessResult // coil3.compose/AsyncImagePainter.State.Success.component2|component2(){}[0]
final fun copy(androidx.compose.ui.graphics.painter/Painter = ..., coil3.request/SuccessResult = ...): coil3.compose/AsyncImagePainter.State.Success // coil3.compose/AsyncImagePainter.State.Success.copy|copy(androidx.compose.ui.graphics.painter.Painter;coil3.request.SuccessResult){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.compose/AsyncImagePainter.State.Success.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // coil3.compose/AsyncImagePainter.State.Success.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // coil3.compose/AsyncImagePainter.State.Success.toString|toString(){}[0]
}
final object Empty : coil3.compose/AsyncImagePainter.State { // coil3.compose/AsyncImagePainter.State.Empty|null[0]
final val painter // coil3.compose/AsyncImagePainter.State.Empty.painter|{}painter[0]
final fun <get-painter>(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/AsyncImagePainter.State.Empty.painter.<get-painter>|<get-painter>(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.compose/AsyncImagePainter.State.Empty.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // coil3.compose/AsyncImagePainter.State.Empty.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // coil3.compose/AsyncImagePainter.State.Empty.toString|toString(){}[0]
}
}
final class Input { // coil3.compose/AsyncImagePainter.Input|null[0]
constructor <init>(coil3/ImageLoader, coil3.request/ImageRequest, coil3.compose/AsyncImageModelEqualityDelegate) // coil3.compose/AsyncImagePainter.Input.<init>|<init>(coil3.ImageLoader;coil3.request.ImageRequest;coil3.compose.AsyncImageModelEqualityDelegate){}[0]
final val imageLoader // coil3.compose/AsyncImagePainter.Input.imageLoader|{}imageLoader[0]
final fun <get-imageLoader>(): coil3/ImageLoader // coil3.compose/AsyncImagePainter.Input.imageLoader.<get-imageLoader>|<get-imageLoader>(){}[0]
final val modelEqualityDelegate // coil3.compose/AsyncImagePainter.Input.modelEqualityDelegate|{}modelEqualityDelegate[0]
final fun <get-modelEqualityDelegate>(): coil3.compose/AsyncImageModelEqualityDelegate // coil3.compose/AsyncImagePainter.Input.modelEqualityDelegate.<get-modelEqualityDelegate>|<get-modelEqualityDelegate>(){}[0]
final val request // coil3.compose/AsyncImagePainter.Input.request|{}request[0]
final fun <get-request>(): coil3.request/ImageRequest // coil3.compose/AsyncImagePainter.Input.request.<get-request>|<get-request>(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.compose/AsyncImagePainter.Input.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // coil3.compose/AsyncImagePainter.Input.hashCode|hashCode(){}[0]
}
final object Companion { // coil3.compose/AsyncImagePainter.Companion|null[0]
final val DefaultTransform // coil3.compose/AsyncImagePainter.Companion.DefaultTransform|{}DefaultTransform[0]
final fun <get-DefaultTransform>(): kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State> // coil3.compose/AsyncImagePainter.Companion.DefaultTransform.<get-DefaultTransform>|<get-DefaultTransform>(){}[0]
}
}
final class coil3.compose/ConstraintsSizeResolver : androidx.compose.ui.layout/LayoutModifier, coil3.size/SizeResolver { // coil3.compose/ConstraintsSizeResolver|null[0]
constructor <init>() // coil3.compose/ConstraintsSizeResolver.<init>|<init>(){}[0]
final fun (androidx.compose.ui.layout/MeasureScope).measure(androidx.compose.ui.layout/Measurable, androidx.compose.ui.unit/Constraints): androidx.compose.ui.layout/MeasureResult // coil3.compose/ConstraintsSizeResolver.measure|measure@androidx.compose.ui.layout.MeasureScope(androidx.compose.ui.layout.Measurable;androidx.compose.ui.unit.Constraints){}[0]
final fun setConstraints(androidx.compose.ui.unit/Constraints) // coil3.compose/ConstraintsSizeResolver.setConstraints|setConstraints(androidx.compose.ui.unit.Constraints){}[0]
final suspend fun size(): coil3.size/Size // coil3.compose/ConstraintsSizeResolver.size|size(){}[0]
}
final class coil3.compose/CrossfadePainter : androidx.compose.ui.graphics.painter/Painter { // coil3.compose/CrossfadePainter|null[0]
constructor <init>(androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.layout/ContentScale = ..., kotlin.time/Duration = ..., kotlin.time/TimeSource = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // coil3.compose/CrossfadePainter.<init>|<init>(androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.layout.ContentScale;kotlin.time.Duration;kotlin.time.TimeSource;kotlin.Boolean;kotlin.Boolean){}[0]
constructor <init>(androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.layout/ContentScale = ..., kotlin.time/Duration = ..., kotlin.time/TimeSource = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // coil3.compose/CrossfadePainter.<init>|<init>(androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.layout.ContentScale;kotlin.time.Duration;kotlin.time.TimeSource;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean){}[0]
final val contentScale // coil3.compose/CrossfadePainter.contentScale|{}contentScale[0]
final fun <get-contentScale>(): androidx.compose.ui.layout/ContentScale // coil3.compose/CrossfadePainter.contentScale.<get-contentScale>|<get-contentScale>(){}[0]
final val duration // coil3.compose/CrossfadePainter.duration|{}duration[0]
final fun <get-duration>(): kotlin.time/Duration // coil3.compose/CrossfadePainter.duration.<get-duration>|<get-duration>(){}[0]
final val end // coil3.compose/CrossfadePainter.end|{}end[0]
final fun <get-end>(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/CrossfadePainter.end.<get-end>|<get-end>(){}[0]
final val fadeStart // coil3.compose/CrossfadePainter.fadeStart|{}fadeStart[0]
final fun <get-fadeStart>(): kotlin/Boolean // coil3.compose/CrossfadePainter.fadeStart.<get-fadeStart>|<get-fadeStart>(){}[0]
final val intrinsicSize // coil3.compose/CrossfadePainter.intrinsicSize|{}intrinsicSize[0]
final fun <get-intrinsicSize>(): androidx.compose.ui.geometry/Size // coil3.compose/CrossfadePainter.intrinsicSize.<get-intrinsicSize>|<get-intrinsicSize>(){}[0]
final val preferEndFirstIntrinsicSize // coil3.compose/CrossfadePainter.preferEndFirstIntrinsicSize|{}preferEndFirstIntrinsicSize[0]
final fun <get-preferEndFirstIntrinsicSize>(): kotlin/Boolean // coil3.compose/CrossfadePainter.preferEndFirstIntrinsicSize.<get-preferEndFirstIntrinsicSize>|<get-preferEndFirstIntrinsicSize>(){}[0]
final val preferExactIntrinsicSize // coil3.compose/CrossfadePainter.preferExactIntrinsicSize|{}preferExactIntrinsicSize[0]
final fun <get-preferExactIntrinsicSize>(): kotlin/Boolean // coil3.compose/CrossfadePainter.preferExactIntrinsicSize.<get-preferExactIntrinsicSize>|<get-preferExactIntrinsicSize>(){}[0]
final val timeSource // coil3.compose/CrossfadePainter.timeSource|{}timeSource[0]
final fun <get-timeSource>(): kotlin.time/TimeSource // coil3.compose/CrossfadePainter.timeSource.<get-timeSource>|<get-timeSource>(){}[0]
final var start // coil3.compose/CrossfadePainter.start|{}start[0]
final fun <get-start>(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/CrossfadePainter.start.<get-start>|<get-start>(){}[0]
}
final class coil3.compose/ImagePainter : androidx.compose.ui.graphics.painter/Painter { // coil3.compose/ImagePainter|null[0]
constructor <init>(coil3/Image) // coil3.compose/ImagePainter.<init>|<init>(coil3.Image){}[0]
final val image // coil3.compose/ImagePainter.image|{}image[0]
final fun <get-image>(): coil3/Image // coil3.compose/ImagePainter.image.<get-image>|<get-image>(){}[0]
final val intrinsicSize // coil3.compose/ImagePainter.intrinsicSize|{}intrinsicSize[0]
final fun <get-intrinsicSize>(): androidx.compose.ui.geometry/Size // coil3.compose/ImagePainter.intrinsicSize.<get-intrinsicSize>|<get-intrinsicSize>(){}[0]
}
final val coil3.compose/LocalAsyncImageModelEqualityDelegate // coil3.compose/LocalAsyncImageModelEqualityDelegate|{}LocalAsyncImageModelEqualityDelegate[0]
final fun <get-LocalAsyncImageModelEqualityDelegate>(): androidx.compose.runtime/ProvidableCompositionLocal<coil3.compose/AsyncImageModelEqualityDelegate> // coil3.compose/LocalAsyncImageModelEqualityDelegate.<get-LocalAsyncImageModelEqualityDelegate>|<get-LocalAsyncImageModelEqualityDelegate>(){}[0]
final val coil3.compose/LocalAsyncImagePreviewHandler // coil3.compose/LocalAsyncImagePreviewHandler|{}LocalAsyncImagePreviewHandler[0]
final fun <get-LocalAsyncImagePreviewHandler>(): androidx.compose.runtime/ProvidableCompositionLocal<coil3.compose/AsyncImagePreviewHandler> // coil3.compose/LocalAsyncImagePreviewHandler.<get-LocalAsyncImagePreviewHandler>|<get-LocalAsyncImagePreviewHandler>(){}[0]
final val coil3.compose/LocalPlatformContext // coil3.compose/LocalPlatformContext|{}LocalPlatformContext[0]
final fun <get-LocalPlatformContext>(): androidx.compose.runtime/ProvidableCompositionLocal<coil3/PlatformContext> // coil3.compose/LocalPlatformContext.<get-LocalPlatformContext>|<get-LocalPlatformContext>(){}[0]
final val coil3.compose/coil3_compose_AsyncImagePainter$stableprop // coil3.compose/coil3_compose_AsyncImagePainter$stableprop|#static{}coil3_compose_AsyncImagePainter$stableprop[0]
final val coil3.compose/coil3_compose_AsyncImagePainter_Input$stableprop // coil3.compose/coil3_compose_AsyncImagePainter_Input$stableprop|#static{}coil3_compose_AsyncImagePainter_Input$stableprop[0]
final val coil3.compose/coil3_compose_AsyncImagePainter_State_Empty$stableprop // coil3.compose/coil3_compose_AsyncImagePainter_State_Empty$stableprop|#static{}coil3_compose_AsyncImagePainter_State_Empty$stableprop[0]
final val coil3.compose/coil3_compose_AsyncImagePainter_State_Error$stableprop // coil3.compose/coil3_compose_AsyncImagePainter_State_Error$stableprop|#static{}coil3_compose_AsyncImagePainter_State_Error$stableprop[0]
final val coil3.compose/coil3_compose_AsyncImagePainter_State_Loading$stableprop // coil3.compose/coil3_compose_AsyncImagePainter_State_Loading$stableprop|#static{}coil3_compose_AsyncImagePainter_State_Loading$stableprop[0]
final val coil3.compose/coil3_compose_AsyncImagePainter_State_Success$stableprop // coil3.compose/coil3_compose_AsyncImagePainter_State_Success$stableprop|#static{}coil3_compose_AsyncImagePainter_State_Success$stableprop[0]
final val coil3.compose/coil3_compose_ConstraintsSizeResolver$stableprop // coil3.compose/coil3_compose_ConstraintsSizeResolver$stableprop|#static{}coil3_compose_ConstraintsSizeResolver$stableprop[0]
final val coil3.compose/coil3_compose_CrossfadePainter$stableprop // coil3.compose/coil3_compose_CrossfadePainter$stableprop|#static{}coil3_compose_CrossfadePainter$stableprop[0]
final val coil3.compose/coil3_compose_ImagePainter$stableprop // coil3.compose/coil3_compose_ImagePainter$stableprop|#static{}coil3_compose_ImagePainter$stableprop[0]
final val coil3.compose/preferEndFirstIntrinsicSize // coil3.compose/preferEndFirstIntrinsicSize|@coil3.Extras.Key.Companion{}preferEndFirstIntrinsicSize[0]
final fun (coil3/Extras.Key.Companion).<get-preferEndFirstIntrinsicSize>(): coil3/Extras.Key<kotlin/Boolean> // coil3.compose/preferEndFirstIntrinsicSize.<get-preferEndFirstIntrinsicSize>|<get-preferEndFirstIntrinsicSize>@coil3.Extras.Key.Companion(){}[0]
final val coil3.compose/preferEndFirstIntrinsicSize // coil3.compose/preferEndFirstIntrinsicSize|@coil3.request.ImageRequest{}preferEndFirstIntrinsicSize[0]
final fun (coil3.request/ImageRequest).<get-preferEndFirstIntrinsicSize>(): kotlin/Boolean // coil3.compose/preferEndFirstIntrinsicSize.<get-preferEndFirstIntrinsicSize>|<get-preferEndFirstIntrinsicSize>@coil3.request.ImageRequest(){}[0]
final val coil3.compose/useExistingImageAsPlaceholder // coil3.compose/useExistingImageAsPlaceholder|@coil3.Extras.Key.Companion{}useExistingImageAsPlaceholder[0]
final fun (coil3/Extras.Key.Companion).<get-useExistingImageAsPlaceholder>(): coil3/Extras.Key<kotlin/Boolean> // coil3.compose/useExistingImageAsPlaceholder.<get-useExistingImageAsPlaceholder>|<get-useExistingImageAsPlaceholder>@coil3.Extras.Key.Companion(){}[0]
final val coil3.compose/useExistingImageAsPlaceholder // coil3.compose/useExistingImageAsPlaceholder|@coil3.request.ImageRequest{}useExistingImageAsPlaceholder[0]
final fun (coil3.request/ImageRequest).<get-useExistingImageAsPlaceholder>(): kotlin/Boolean // coil3.compose/useExistingImageAsPlaceholder.<get-useExistingImageAsPlaceholder>|<get-useExistingImageAsPlaceholder>@coil3.request.ImageRequest(){}[0]
final fun (coil3.compose/SubcomposeAsyncImageScope).coil3.compose/SubcomposeAsyncImageContent(androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, kotlin/String?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImageContent|SubcomposeAsyncImageContent@coil3.compose.SubcomposeAsyncImageScope(androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;kotlin.String?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun (coil3.request/ImageRequest.Builder).coil3.compose/preferEndFirstIntrinsicSize(kotlin/Boolean): coil3.request/ImageRequest.Builder // coil3.compose/preferEndFirstIntrinsicSize|preferEndFirstIntrinsicSize@coil3.request.ImageRequest.Builder(kotlin.Boolean){}[0]
final fun (coil3.request/ImageRequest.Builder).coil3.compose/useExistingImageAsPlaceholder(kotlin/Boolean): coil3.request/ImageRequest.Builder // coil3.compose/useExistingImageAsPlaceholder|useExistingImageAsPlaceholder@coil3.request.ImageRequest.Builder(kotlin.Boolean){}[0]
final fun (coil3/Image).coil3.compose/asPainter(coil3/PlatformContext, androidx.compose.ui.graphics/FilterQuality = ...): androidx.compose.ui.graphics.painter/Painter // coil3.compose/asPainter|asPainter@coil3.Image(coil3.PlatformContext;androidx.compose.ui.graphics.FilterQuality){}[0]
final fun (coil3/ImageLoader.Builder).coil3.compose/preferEndFirstIntrinsicSize(kotlin/Boolean): coil3/ImageLoader.Builder // coil3.compose/preferEndFirstIntrinsicSize|preferEndFirstIntrinsicSize@coil3.ImageLoader.Builder(kotlin.Boolean){}[0]
final fun (coil3/ImageLoader.Builder).coil3.compose/useExistingImageAsPlaceholder(kotlin/Boolean): coil3/ImageLoader.Builder // coil3.compose/useExistingImageAsPlaceholder|useExistingImageAsPlaceholder@coil3.ImageLoader.Builder(kotlin.Boolean){}[0]
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/DrawScopeSizeResolver(): coil3.compose/DrawScopeSizeResolver // coil3.compose/DrawScopeSizeResolver|DrawScopeSizeResolver(){}[0]
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, kotlin/Function3<coil3.compose/SubcomposeAsyncImageScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;kotlin.Function3<coil3.compose.SubcomposeAsyncImageScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, kotlin/Function3<coil3.compose/SubcomposeAsyncImageScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;kotlin.Function3<coil3.compose.SubcomposeAsyncImageScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Loading, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Success, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Error, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Loading,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Success,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Error,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Loading, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Success, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Error, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Loading,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Success,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Error,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/coil3_compose_AsyncImagePainter$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_AsyncImagePainter$stableprop_getter|coil3_compose_AsyncImagePainter$stableprop_getter(){}[0]
final fun coil3.compose/coil3_compose_AsyncImagePainter_Input$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_AsyncImagePainter_Input$stableprop_getter|coil3_compose_AsyncImagePainter_Input$stableprop_getter(){}[0]
final fun coil3.compose/coil3_compose_AsyncImagePainter_State_Empty$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_AsyncImagePainter_State_Empty$stableprop_getter|coil3_compose_AsyncImagePainter_State_Empty$stableprop_getter(){}[0]
final fun coil3.compose/coil3_compose_AsyncImagePainter_State_Error$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_AsyncImagePainter_State_Error$stableprop_getter|coil3_compose_AsyncImagePainter_State_Error$stableprop_getter(){}[0]
final fun coil3.compose/coil3_compose_AsyncImagePainter_State_Loading$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_AsyncImagePainter_State_Loading$stableprop_getter|coil3_compose_AsyncImagePainter_State_Loading$stableprop_getter(){}[0]
final fun coil3.compose/coil3_compose_AsyncImagePainter_State_Success$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_AsyncImagePainter_State_Success$stableprop_getter|coil3_compose_AsyncImagePainter_State_Success$stableprop_getter(){}[0]
final fun coil3.compose/coil3_compose_ConstraintsSizeResolver$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_ConstraintsSizeResolver$stableprop_getter|coil3_compose_ConstraintsSizeResolver$stableprop_getter(){}[0]
final fun coil3.compose/coil3_compose_CrossfadePainter$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_CrossfadePainter$stableprop_getter|coil3_compose_CrossfadePainter$stableprop_getter(){}[0]
final fun coil3.compose/coil3_compose_ImagePainter$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_ImagePainter$stableprop_getter|coil3_compose_ImagePainter$stableprop_getter(){}[0]
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, coil3/ImageLoader, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;coil3.ImageLoader;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, coil3/ImageLoader, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;coil3.ImageLoader;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, coil3/ImageLoader, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;coil3.ImageLoader;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, coil3/ImageLoader, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;coil3.ImageLoader;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/rememberConstraintsSizeResolver(androidx.compose.runtime/Composer?, kotlin/Int): coil3.compose/ConstraintsSizeResolver // coil3.compose/rememberConstraintsSizeResolver|rememberConstraintsSizeResolver(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun coil3.compose/rememberDrawScopeSizeResolver(androidx.compose.runtime/Composer?, kotlin/Int): coil3.compose/DrawScopeSizeResolver // coil3.compose/rememberDrawScopeSizeResolver|rememberDrawScopeSizeResolver(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final inline fun coil3.compose/AsyncImagePreviewHandler(crossinline kotlin.coroutines/SuspendFunction1<coil3.request/ImageRequest, coil3/Image>): coil3.compose/AsyncImagePreviewHandler // coil3.compose/AsyncImagePreviewHandler|AsyncImagePreviewHandler(kotlin.coroutines.SuspendFunction1<coil3.request.ImageRequest,coil3.Image>){}[0]
final inline fun coil3.compose/AsyncImagePreviewHandler(crossinline kotlin.coroutines/SuspendFunction1<coil3.request/ImageRequest, coil3/Image?>): coil3.compose/AsyncImagePreviewHandler // coil3.compose/AsyncImagePreviewHandler|AsyncImagePreviewHandler(kotlin.coroutines.SuspendFunction1<coil3.request.ImageRequest,coil3.Image?>){}[0]
+69
View File
@@ -0,0 +1,69 @@
// :coil:coil-compose-core - AsyncImage / rememberAsyncImagePainter internals, vendored
// VERBATIM from coil-kt/coil. Upstream publishes NO desktop-native klibs for the
// compose layer at all (neither mingwX64 nor linux), so this whole module exists to
// give the port its targets. Compose coords substitute to the port's project modules
// on every native configuration (root build's commonization bridge).
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.plugin.compose)
}
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
kotlin {
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
applyDefaultHierarchyTemplate()
sourceSets {
commonMain {
dependencies {
api(project(":coil:coil-core"))
// Declared DIRECTLY, not leaned on transitively: these coords are
// SUBSTITUTED to the port's project modules on native configurations,
// and KGP's granular-metadata visibility rule then drops the substituted
// module's transitives from the commonMain metadata classpath (CLAUDE.md
// "Common pitfalls"). Every artifact the vendored common code touches
// needs its own declaration + its own bridge rule.
api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
api("org.jetbrains.compose.ui:ui:${libs.versions.compose.get()}")
api("org.jetbrains.compose.ui:ui-graphics:${libs.versions.compose.get()}")
api("org.jetbrains.compose.ui:ui-text:${libs.versions.compose.get()}")
api("org.jetbrains.compose.ui:ui-unit:${libs.versions.compose.get()}")
api("org.jetbrains.compose.ui:ui-geometry:${libs.versions.compose.get()}")
api("org.jetbrains.compose.ui:ui-util:${libs.versions.compose.get()}")
api("org.jetbrains.compose.foundation:foundation:${libs.versions.compose.get()}")
api("org.jetbrains.compose.foundation:foundation-layout:${libs.versions.compose.get()}")
}
kotlin.srcDir("src/vendor/common/kotlin")
}
// Upstream's nonAndroid compose layer reaches straight into org.jetbrains.skia
// (Canvas / Bitmap.asComposeImageBitmap), so it needs skiko on its own compile
// classpath - official for macOS/Linux, the bitsycore FORK for mingwX64, on two
// PARALLEL branches. See coil/coil-core/build.gradle.kts.
fun nonAndroidBranch(inName: String, inSkiko: Any) = create("${inName}NonAndroidMain") {
dependsOn(commonMain.get())
kotlin.srcDir("src/vendor/nonAndroid/kotlin")
dependencies { implementation(inSkiko) }
}
val vOfficial = nonAndroidBranch("coilComposeOfficial", libs.skiko)
get("macosArm64Main").dependsOn(vOfficial)
get("linuxX64Main").dependsOn(vOfficial)
get("linuxArm64Main").dependsOn(vOfficial)
if (vHostSupportsMingw) {
val vFork = nonAndroidBranch(
"coilComposeFork",
"com.bitsycore.skiko:skiko:${providers.gradleProperty("skikoMingwVersion").getOrElse(libs.versions.skikoMingw.get())}",
)
get("mingwX64Main").dependsOn(vFork)
}
}
compilerOptions {
freeCompilerArgs.addAll(
"-Xexpect-actual-classes",
"-opt-in=coil3.annotation.InternalCoilApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=coil3.annotation.DelicateCoilApi",
)
}
}
+44
View File
@@ -0,0 +1,44 @@
# :coil:coil-compose-core - AsyncImage / rememberAsyncImagePainter internals.
# Upstream publishes NEITHER mingwX64 NOR linux for the compose layer.
# Upstream source sets collapse onto this port: it has NO android, jvm or js
# target, so nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all
# apply to EVERY target here. They land in TWO trees, not one: the non* sets keep
# their own level (src/vendor/nonAndroid) ABOVE src/vendor/native, because some of
# them declare `expect`s whose `actual`s live in upstream nativeMain - collapsing
# both into one source set makes expect and actual siblings, which is an error. appleMain is macosArm64
# only and nonAppleMain is linux + mingw, so those two keep their own trees (the
# build file wires a `nonAppleMain` intermediate; appleMain comes from the default
# hierarchy template).
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
SET_FOLDER=coil-compose-core/src
commonMain/kotlin/ -> src/vendor/common/kotlin/
# | commonMain/kotlin/coil3/compose/AsyncImage.kt -> src/vendor/common/kotlin/coil3/compose/AsyncImage.kt
# | commonMain/kotlin/coil3/compose/AsyncImagePainter.kt -> src/vendor/common/kotlin/coil3/compose/AsyncImagePainter.kt
# | commonMain/kotlin/coil3/compose/ConstraintsSizeResolver.kt -> src/vendor/common/kotlin/coil3/compose/ConstraintsSizeResolver.kt
# | commonMain/kotlin/coil3/compose/CrossfadePainter.kt -> src/vendor/common/kotlin/coil3/compose/CrossfadePainter.kt
# | commonMain/kotlin/coil3/compose/DrawScopeSizeResolver.kt -> src/vendor/common/kotlin/coil3/compose/DrawScopeSizeResolver.kt
# | commonMain/kotlin/coil3/compose/ImagePainter.kt -> src/vendor/common/kotlin/coil3/compose/ImagePainter.kt
# | commonMain/kotlin/coil3/compose/LocalAsyncImageModelEqualityDelegate.kt -> src/vendor/common/kotlin/coil3/compose/LocalAsyncImageModelEqualityDelegate.kt
# | commonMain/kotlin/coil3/compose/LocalAsyncImagePreviewHandler.kt -> src/vendor/common/kotlin/coil3/compose/LocalAsyncImagePreviewHandler.kt
# | commonMain/kotlin/coil3/compose/LocalPlatformContext.kt -> src/vendor/common/kotlin/coil3/compose/LocalPlatformContext.kt
# | commonMain/kotlin/coil3/compose/SubcomposeAsyncImage.kt -> src/vendor/common/kotlin/coil3/compose/SubcomposeAsyncImage.kt
# | commonMain/kotlin/coil3/compose/imageRequests.kt -> src/vendor/common/kotlin/coil3/compose/imageRequests.kt
# | commonMain/kotlin/coil3/compose/internal/ContentPainterModifier.kt -> src/vendor/common/kotlin/coil3/compose/internal/ContentPainterModifier.kt
# | commonMain/kotlin/coil3/compose/internal/DeferredDispatch.kt -> src/vendor/common/kotlin/coil3/compose/internal/DeferredDispatch.kt
# | commonMain/kotlin/coil3/compose/internal/ForwardingCoroutineContext.kt -> src/vendor/common/kotlin/coil3/compose/internal/ForwardingCoroutineContext.kt
# | commonMain/kotlin/coil3/compose/internal/utils.kt -> src/vendor/common/kotlin/coil3/compose/internal/utils.kt
nonAndroidMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
# | nonAndroidMain/kotlin/coil3/compose/AsyncImagePainter.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/compose/AsyncImagePainter.nonAndroid.kt
# | nonAndroidMain/kotlin/coil3/compose/ImagePainter.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/compose/ImagePainter.nonAndroid.kt
# | nonAndroidMain/kotlin/coil3/compose/LocalPlatformContext.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/compose/LocalPlatformContext.nonAndroid.kt
# | nonAndroidMain/kotlin/coil3/compose/internal/utils.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/compose/internal/utils.nonAndroid.kt
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
# androidMain (4)
# | androidMain/kotlin/coil3/compose/AsyncImagePainter.android.kt -> src/vendor/android/kotlin/coil3/compose/AsyncImagePainter.android.kt
# | androidMain/kotlin/coil3/compose/ImagePainter.android.kt -> src/vendor/android/kotlin/coil3/compose/ImagePainter.android.kt
# | androidMain/kotlin/coil3/compose/LocalPlatformContext.android.kt -> src/vendor/android/kotlin/coil3/compose/LocalPlatformContext.android.kt
# | androidMain/kotlin/coil3/compose/internal/utils.android.kt -> src/vendor/android/kotlin/coil3/compose/internal/utils.android.kt
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
@@ -0,0 +1,21 @@
// Klib ABI Dump
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.coil3:coil-compose>
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, kotlin/Function3<coil3.compose/SubcomposeAsyncImageScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;kotlin.Function3<coil3.compose.SubcomposeAsyncImageScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, kotlin/Function3<coil3.compose/SubcomposeAsyncImageScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;kotlin.Function3<coil3.compose.SubcomposeAsyncImageScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Loading, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Success, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Error, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Loading,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Success,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Error,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Loading, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Success, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Error, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Loading,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Success,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Error,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun coil3.compose/setSingletonImageLoaderFactory(kotlin/Function1<coil3/PlatformContext, coil3/ImageLoader>, androidx.compose.runtime/Composer?, kotlin/Int) // coil3.compose/setSingletonImageLoaderFactory|setSingletonImageLoaderFactory(kotlin.Function1<coil3.PlatformContext,coil3.ImageLoader>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
+31
View File
@@ -0,0 +1,31 @@
// :coil:coil-compose - the public compose entry points (AsyncImage, SubcomposeAsyncImage),
// vendored VERBATIM from coil-kt/coil. commonMain only. Upstream publishes no
// desktop-native klibs for the compose layer.
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.plugin.compose)
}
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
kotlin {
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
applyDefaultHierarchyTemplate()
sourceSets {
commonMain {
dependencies {
api(project(":coil:coil"))
api(project(":coil:coil-compose-core"))
}
kotlin.srcDir("src/vendor/common/kotlin")
}
}
compilerOptions {
freeCompilerArgs.addAll(
"-Xexpect-actual-classes",
"-opt-in=coil3.annotation.InternalCoilApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=coil3.annotation.DelicateCoilApi",
)
}
}
+15
View File
@@ -0,0 +1,15 @@
# :coil:coil-compose - the public compose entry points. commonMain only.
# Upstream publishes NEITHER mingwX64 NOR linux for the compose layer.
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
SET_FOLDER=coil-compose/src
commonMain/kotlin/ -> src/vendor/common/kotlin/
# | commonMain/kotlin/coil3/compose/SingletonAsyncImage.kt -> src/vendor/common/kotlin/coil3/compose/SingletonAsyncImage.kt
# | commonMain/kotlin/coil3/compose/SingletonAsyncImagePainter.kt -> src/vendor/common/kotlin/coil3/compose/SingletonAsyncImagePainter.kt
# | commonMain/kotlin/coil3/compose/SingletonSubcomposeAsyncImage.kt -> src/vendor/common/kotlin/coil3/compose/SingletonSubcomposeAsyncImage.kt
# | commonMain/kotlin/coil3/compose/singletonImageLoaders.kt -> src/vendor/common/kotlin/coil3/compose/singletonImageLoaders.kt
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
# (none - every source set under SET_FOLDER is vendored)
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
File diff suppressed because it is too large Load Diff
+90
View File
@@ -0,0 +1,90 @@
// :coil:coil-core - coil3.* core, vendored VERBATIM from coil-kt/coil into src/vendor.
//
// Upstream publishes linux + apple klibs but NOT mingwX64, and the code is
// target-agnostic, so the port mostly just re-declares the targets.
//
// SOURCE-SET COLLAPSE: this module has no android, jvm or js target, so upstream's
// nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all apply to every
// target here and are vendored into ONE tree (src/vendor/native). Only the
// apple-vs-rest split survives: appleMain is macosArm64, nonAppleMain is linux+mingw.
//
// SKIA: upstream's non-android decoder is skiko-based (SkiaImageDecoder), which is
// this port's renderer already. macOS/Linux take the official skiko; mingwX64 takes
// the bitsycore FORK, the same swap :compose:ui:ui-graphics makes.
//
// Provenance = compose-fork.txt + scripts/compose-fork/compose.properties. Never
// hand-edit src/vendor - change the manifest + re-run
// `python scripts/compose-fork/sync.py coil/coil-core`.
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
kotlin {
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
applyDefaultHierarchyTemplate()
sourceSets {
commonMain {
dependencies {
api(libs.kotlinx.coroutines.core)
api(libs.okio)
}
kotlin.srcDir("src/vendor/common/kotlin")
}
// SKIKO BRANCHES. Upstream's nonAndroid files use org.jetbrains.skia.*, so the
// intermediate source set that holds them needs skiko on its own compile
// classpath (the shared-metadata compilation is a real compile). macOS/Linux
// take the OFFICIAL skiko; mingwX64 takes the bitsycore FORK - and the two
// cannot share an intermediate, so mingw gets a PARALLEL branch over the same
// srcDirs. Same shape as :compose:ui:ui-graphics.
//
// Each branch is two levels deep because upstream's nonAndroid sets declare
// `expect`s whose `actual`s live in nativeMain - siblings in one set is an error.
fun skikoBranch(inName: String, inSkiko: Any): org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet {
val vNonAndroid = create("${inName}NonAndroidMain") {
dependsOn(commonMain.get())
kotlin.srcDir("src/vendor/nonAndroid/kotlin")
dependencies { implementation(inSkiko) }
}
return create("${inName}NativeMain") {
dependsOn(vNonAndroid)
kotlin.srcDir("src/vendor/native/kotlin")
}
}
val vOfficial = skikoBranch("coilOfficial", libs.skiko)
val vAppleMain = create("coilAppleMain") {
dependsOn(vOfficial)
kotlin.srcDir("src/vendor/apple/kotlin")
}
val vLinuxMain = create("coilLinuxMain") {
dependsOn(vOfficial)
kotlin.srcDir("src/vendor/nonApple/kotlin")
}
get("macosArm64Main").dependsOn(vAppleMain)
get("linuxX64Main").dependsOn(vLinuxMain)
get("linuxArm64Main").dependsOn(vLinuxMain)
if (vHostSupportsMingw) {
val vFork = skikoBranch(
"coilFork",
"com.bitsycore.skiko:skiko:${providers.gradleProperty("skikoMingwVersion").getOrElse(libs.versions.skikoMingw.get())}",
)
val vMingwMain = create("coilMingwMain") {
dependsOn(vFork)
kotlin.srcDir("src/vendor/nonApple/kotlin")
}
get("mingwX64Main").dependsOn(vMingwMain)
}
}
compilerOptions {
freeCompilerArgs.addAll(
"-Xexpect-actual-classes",
"-opt-in=kotlinx.cinterop.ExperimentalForeignApi",
"-opt-in=coil3.annotation.InternalCoilApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=coil3.annotation.DelicateCoilApi",
)
}
}
+206
View File
@@ -0,0 +1,206 @@
# :coil:coil-core - coil3.* core, vendored verbatim from coil-kt/coil.
# Upstream publishes linux + apple but NOT mingwX64, so the port re-declares the
# targets and rebuilds the same sources. Decoding is Skia-based upstream
# (SkiaImageDecoder), which is exactly this port's renderer - on mingwX64 the
# skiko dependency is the bitsycore FORK, as in :compose:ui:ui-graphics.
# Upstream source sets collapse onto this port: it has NO android, jvm or js
# target, so nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all
# apply to EVERY target here. They land in TWO trees, not one: the non* sets keep
# their own level (src/vendor/nonAndroid) ABOVE src/vendor/native, because some of
# them declare `expect`s whose `actual`s live in upstream nativeMain - collapsing
# both into one source set makes expect and actual siblings, which is an error. appleMain is macosArm64
# only and nonAppleMain is linux + mingw, so those two keep their own trees (the
# build file wires a `nonAppleMain` intermediate; appleMain comes from the default
# hierarchy template).
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
SET_FOLDER=coil-core/src
commonMain/kotlin/ -> src/vendor/common/kotlin/
# | commonMain/kotlin/coil3/ColorImage.kt -> src/vendor/common/kotlin/coil3/ColorImage.kt
# | commonMain/kotlin/coil3/ComponentRegistry.kt -> src/vendor/common/kotlin/coil3/ComponentRegistry.kt
# | commonMain/kotlin/coil3/EventListener.kt -> src/vendor/common/kotlin/coil3/EventListener.kt
# | commonMain/kotlin/coil3/Extras.kt -> src/vendor/common/kotlin/coil3/Extras.kt
# | commonMain/kotlin/coil3/Image.kt -> src/vendor/common/kotlin/coil3/Image.kt
# | commonMain/kotlin/coil3/ImageLoader.kt -> src/vendor/common/kotlin/coil3/ImageLoader.kt
# | commonMain/kotlin/coil3/PlatformContext.kt -> src/vendor/common/kotlin/coil3/PlatformContext.kt
# | commonMain/kotlin/coil3/RealImageLoader.kt -> src/vendor/common/kotlin/coil3/RealImageLoader.kt
# | commonMain/kotlin/coil3/Uri.kt -> src/vendor/common/kotlin/coil3/Uri.kt
# | commonMain/kotlin/coil3/annotation/annotations.kt -> src/vendor/common/kotlin/coil3/annotation/annotations.kt
# | commonMain/kotlin/coil3/decode/BlackholeDecoder.kt -> src/vendor/common/kotlin/coil3/decode/BlackholeDecoder.kt
# | commonMain/kotlin/coil3/decode/DataSource.kt -> src/vendor/common/kotlin/coil3/decode/DataSource.kt
# | commonMain/kotlin/coil3/decode/DecodeResult.kt -> src/vendor/common/kotlin/coil3/decode/DecodeResult.kt
# | commonMain/kotlin/coil3/decode/DecodeUtils.kt -> src/vendor/common/kotlin/coil3/decode/DecodeUtils.kt
# | commonMain/kotlin/coil3/decode/Decoder.kt -> src/vendor/common/kotlin/coil3/decode/Decoder.kt
# | commonMain/kotlin/coil3/decode/ImageSource.kt -> src/vendor/common/kotlin/coil3/decode/ImageSource.kt
# | commonMain/kotlin/coil3/disk/DiskCache.kt -> src/vendor/common/kotlin/coil3/disk/DiskCache.kt
# | commonMain/kotlin/coil3/disk/DiskLruCache.kt -> src/vendor/common/kotlin/coil3/disk/DiskLruCache.kt
# | commonMain/kotlin/coil3/disk/FaultHidingSink.kt -> src/vendor/common/kotlin/coil3/disk/FaultHidingSink.kt
# | commonMain/kotlin/coil3/disk/RealDiskCache.kt -> src/vendor/common/kotlin/coil3/disk/RealDiskCache.kt
# | commonMain/kotlin/coil3/disk/utils.kt -> src/vendor/common/kotlin/coil3/disk/utils.kt
# | commonMain/kotlin/coil3/fetch/BitmapFetcher.kt -> src/vendor/common/kotlin/coil3/fetch/BitmapFetcher.kt
# | commonMain/kotlin/coil3/fetch/ByteArrayFetcher.kt -> src/vendor/common/kotlin/coil3/fetch/ByteArrayFetcher.kt
# | commonMain/kotlin/coil3/fetch/DataUriFetcher.kt -> src/vendor/common/kotlin/coil3/fetch/DataUriFetcher.kt
# | commonMain/kotlin/coil3/fetch/FetchResult.kt -> src/vendor/common/kotlin/coil3/fetch/FetchResult.kt
# | commonMain/kotlin/coil3/fetch/Fetcher.kt -> src/vendor/common/kotlin/coil3/fetch/Fetcher.kt
# | commonMain/kotlin/coil3/fetch/FileUriFetcher.kt -> src/vendor/common/kotlin/coil3/fetch/FileUriFetcher.kt
# | commonMain/kotlin/coil3/imageLoaders.kt -> src/vendor/common/kotlin/coil3/imageLoaders.kt
# | commonMain/kotlin/coil3/intercept/EngineInterceptor.kt -> src/vendor/common/kotlin/coil3/intercept/EngineInterceptor.kt
# | commonMain/kotlin/coil3/intercept/Interceptor.kt -> src/vendor/common/kotlin/coil3/intercept/Interceptor.kt
# | commonMain/kotlin/coil3/intercept/RealInterceptorChain.kt -> src/vendor/common/kotlin/coil3/intercept/RealInterceptorChain.kt
# | commonMain/kotlin/coil3/key/FileUriKeyer.kt -> src/vendor/common/kotlin/coil3/key/FileUriKeyer.kt
# | commonMain/kotlin/coil3/key/Keyer.kt -> src/vendor/common/kotlin/coil3/key/Keyer.kt
# | commonMain/kotlin/coil3/key/UriKeyer.kt -> src/vendor/common/kotlin/coil3/key/UriKeyer.kt
# | commonMain/kotlin/coil3/map/Mapper.kt -> src/vendor/common/kotlin/coil3/map/Mapper.kt
# | commonMain/kotlin/coil3/map/PathMapper.kt -> src/vendor/common/kotlin/coil3/map/PathMapper.kt
# | commonMain/kotlin/coil3/map/StringMapper.kt -> src/vendor/common/kotlin/coil3/map/StringMapper.kt
# | commonMain/kotlin/coil3/memory/MemoryCache.kt -> src/vendor/common/kotlin/coil3/memory/MemoryCache.kt
# | commonMain/kotlin/coil3/memory/MemoryCacheService.kt -> src/vendor/common/kotlin/coil3/memory/MemoryCacheService.kt
# | commonMain/kotlin/coil3/memory/RealMemoryCache.kt -> src/vendor/common/kotlin/coil3/memory/RealMemoryCache.kt
# | commonMain/kotlin/coil3/memory/StrongMemoryCache.kt -> src/vendor/common/kotlin/coil3/memory/StrongMemoryCache.kt
# | commonMain/kotlin/coil3/memory/WeakMemoryCache.kt -> src/vendor/common/kotlin/coil3/memory/WeakMemoryCache.kt
# | commonMain/kotlin/coil3/request/CachePolicy.kt -> src/vendor/common/kotlin/coil3/request/CachePolicy.kt
# | commonMain/kotlin/coil3/request/Disposable.kt -> src/vendor/common/kotlin/coil3/request/Disposable.kt
# | commonMain/kotlin/coil3/request/ImageRequest.kt -> src/vendor/common/kotlin/coil3/request/ImageRequest.kt
# | commonMain/kotlin/coil3/request/ImageResult.kt -> src/vendor/common/kotlin/coil3/request/ImageResult.kt
# | commonMain/kotlin/coil3/request/NullRequestData.kt -> src/vendor/common/kotlin/coil3/request/NullRequestData.kt
# | commonMain/kotlin/coil3/request/NullRequestDataException.kt -> src/vendor/common/kotlin/coil3/request/NullRequestDataException.kt
# | commonMain/kotlin/coil3/request/Options.kt -> src/vendor/common/kotlin/coil3/request/Options.kt
# | commonMain/kotlin/coil3/request/RequestDelegate.kt -> src/vendor/common/kotlin/coil3/request/RequestDelegate.kt
# | commonMain/kotlin/coil3/request/RequestService.kt -> src/vendor/common/kotlin/coil3/request/RequestService.kt
# | commonMain/kotlin/coil3/request/imageRequests.kt -> src/vendor/common/kotlin/coil3/request/imageRequests.kt
# | commonMain/kotlin/coil3/size/Dimension.kt -> src/vendor/common/kotlin/coil3/size/Dimension.kt
# | commonMain/kotlin/coil3/size/Precision.kt -> src/vendor/common/kotlin/coil3/size/Precision.kt
# | commonMain/kotlin/coil3/size/RealSizeResolver.kt -> src/vendor/common/kotlin/coil3/size/RealSizeResolver.kt
# | commonMain/kotlin/coil3/size/Scale.kt -> src/vendor/common/kotlin/coil3/size/Scale.kt
# | commonMain/kotlin/coil3/size/Size.kt -> src/vendor/common/kotlin/coil3/size/Size.kt
# | commonMain/kotlin/coil3/size/SizeResolver.kt -> src/vendor/common/kotlin/coil3/size/SizeResolver.kt
# | commonMain/kotlin/coil3/target/Target.kt -> src/vendor/common/kotlin/coil3/target/Target.kt
# | commonMain/kotlin/coil3/transform/Transformation.kt -> src/vendor/common/kotlin/coil3/transform/Transformation.kt
# | commonMain/kotlin/coil3/util/LruCache.kt -> src/vendor/common/kotlin/coil3/util/LruCache.kt
# | commonMain/kotlin/coil3/util/ServiceLoaderComponentRegistry.kt -> src/vendor/common/kotlin/coil3/util/ServiceLoaderComponentRegistry.kt
# | commonMain/kotlin/coil3/util/SystemCallbacks.kt -> src/vendor/common/kotlin/coil3/util/SystemCallbacks.kt
# | commonMain/kotlin/coil3/util/collections.kt -> src/vendor/common/kotlin/coil3/util/collections.kt
# | commonMain/kotlin/coil3/util/contexts.kt -> src/vendor/common/kotlin/coil3/util/contexts.kt
# | commonMain/kotlin/coil3/util/coroutines.kt -> src/vendor/common/kotlin/coil3/util/coroutines.kt
# | commonMain/kotlin/coil3/util/fileSystems.kt -> src/vendor/common/kotlin/coil3/util/fileSystems.kt
# | commonMain/kotlin/coil3/util/logging.kt -> src/vendor/common/kotlin/coil3/util/logging.kt
# | commonMain/kotlin/coil3/util/mimeTypes.kt -> src/vendor/common/kotlin/coil3/util/mimeTypes.kt
# | commonMain/kotlin/coil3/util/utils.kt -> src/vendor/common/kotlin/coil3/util/utils.kt
nonAndroidMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
# | nonAndroidMain/kotlin/coil3/ColorImage.kt -> src/vendor/nonAndroid/kotlin/coil3/ColorImage.kt
# | nonAndroidMain/kotlin/coil3/EventListener.kt -> src/vendor/nonAndroid/kotlin/coil3/EventListener.kt
# | nonAndroidMain/kotlin/coil3/Image.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/Image.nonAndroid.kt
# | nonAndroidMain/kotlin/coil3/PlatformContext.kt -> src/vendor/nonAndroid/kotlin/coil3/PlatformContext.kt
# | nonAndroidMain/kotlin/coil3/RealImageLoader.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/RealImageLoader.nonAndroid.kt
# | nonAndroidMain/kotlin/coil3/decode/SkiaImageDecoder.kt -> src/vendor/nonAndroid/kotlin/coil3/decode/SkiaImageDecoder.kt
# | nonAndroidMain/kotlin/coil3/intercept/EngineInterceptor.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/intercept/EngineInterceptor.nonAndroid.kt
# | nonAndroidMain/kotlin/coil3/request/RequestService.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/request/RequestService.nonAndroid.kt
# | nonAndroidMain/kotlin/coil3/request/imageRequests.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/request/imageRequests.nonAndroid.kt
# | nonAndroidMain/kotlin/coil3/util/SystemCallbacks.kt -> src/vendor/nonAndroid/kotlin/coil3/util/SystemCallbacks.kt
# | nonAndroidMain/kotlin/coil3/util/contexts.kt -> src/vendor/nonAndroid/kotlin/coil3/util/contexts.kt
# | nonAndroidMain/kotlin/coil3/util/mimeTypes.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/util/mimeTypes.nonAndroid.kt
# | nonAndroidMain/kotlin/coil3/util/utils.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/util/utils.nonAndroid.kt
nonJvmCommonMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
# | nonJvmCommonMain/kotlin/coil3/RealImageLoader.nonJvmCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/RealImageLoader.nonJvmCommon.kt
# | nonJvmCommonMain/kotlin/coil3/util/ServiceLoaderComponentRegistry.kt -> src/vendor/nonAndroid/kotlin/coil3/util/ServiceLoaderComponentRegistry.kt
# | nonJvmCommonMain/kotlin/coil3/util/collections.nonJvmCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/util/collections.nonJvmCommon.kt
# | nonJvmCommonMain/kotlin/coil3/util/contexts.nonJvmCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/util/contexts.nonJvmCommon.kt
# | nonJvmCommonMain/kotlin/coil3/util/fileSystems.nonJvmCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/util/fileSystems.nonJvmCommon.kt
nonJsCommonMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
# | nonJsCommonMain/kotlin/coil3/disk/utils.kt -> src/vendor/nonAndroid/kotlin/coil3/disk/utils.kt
# | nonJsCommonMain/kotlin/coil3/imageLoaders.nonJsCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/imageLoaders.nonJsCommon.kt
# | nonJsCommonMain/kotlin/coil3/util/coroutines.nonJsCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/util/coroutines.nonJsCommon.kt
# | nonJsCommonMain/kotlin/coil3/util/fileSystems.nonJsCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/util/fileSystems.nonJsCommon.kt
nativeMain/kotlin/ -> src/vendor/native/kotlin/
# | nativeMain/kotlin/coil3/decode/SkiaImageDecoder.native.kt -> src/vendor/native/kotlin/coil3/decode/SkiaImageDecoder.native.kt
# | nativeMain/kotlin/coil3/util/utils.native.kt -> src/vendor/native/kotlin/coil3/util/utils.native.kt
appleMain/kotlin/ -> src/vendor/apple/kotlin/
# | appleMain/kotlin/coil3/RealImageLoader.apple.kt -> src/vendor/apple/kotlin/coil3/RealImageLoader.apple.kt
# | appleMain/kotlin/coil3/map/NSURLMapper.kt -> src/vendor/apple/kotlin/coil3/map/NSURLMapper.kt
nonAppleMain/kotlin/ -> src/vendor/nonApple/kotlin/
# | nonAppleMain/kotlin/coil3/RealImageLoader.nonApple.kt -> src/vendor/nonApple/kotlin/coil3/RealImageLoader.nonApple.kt
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
# androidMain (49)
# | androidMain/kotlin/coil3/ColorImage.kt -> src/vendor/android/kotlin/coil3/ColorImage.kt
# | androidMain/kotlin/coil3/EventListener.kt -> src/vendor/android/kotlin/coil3/EventListener.kt
# | androidMain/kotlin/coil3/Image.android.kt -> src/vendor/android/kotlin/coil3/Image.android.kt
# | androidMain/kotlin/coil3/PlatformContext.kt -> src/vendor/android/kotlin/coil3/PlatformContext.kt
# | androidMain/kotlin/coil3/RealImageLoader.android.kt -> src/vendor/android/kotlin/coil3/RealImageLoader.android.kt
# | androidMain/kotlin/coil3/Uri.android.kt -> src/vendor/android/kotlin/coil3/Uri.android.kt
# | androidMain/kotlin/coil3/decode/BitmapFactoryDecoder.kt -> src/vendor/android/kotlin/coil3/decode/BitmapFactoryDecoder.kt
# | androidMain/kotlin/coil3/decode/ExifOrientationStrategy.kt -> src/vendor/android/kotlin/coil3/decode/ExifOrientationStrategy.kt
# | androidMain/kotlin/coil3/decode/ExifUtils.kt -> src/vendor/android/kotlin/coil3/decode/ExifUtils.kt
# | androidMain/kotlin/coil3/decode/ImageSource.kt -> src/vendor/android/kotlin/coil3/decode/ImageSource.kt
# | androidMain/kotlin/coil3/decode/StaticImageDecoder.kt -> src/vendor/android/kotlin/coil3/decode/StaticImageDecoder.kt
# | androidMain/kotlin/coil3/fetch/AssetUriFetcher.kt -> src/vendor/android/kotlin/coil3/fetch/AssetUriFetcher.kt
# | androidMain/kotlin/coil3/fetch/ContentUriFetcher.kt -> src/vendor/android/kotlin/coil3/fetch/ContentUriFetcher.kt
# | androidMain/kotlin/coil3/fetch/DrawableFetcher.kt -> src/vendor/android/kotlin/coil3/fetch/DrawableFetcher.kt
# | androidMain/kotlin/coil3/fetch/ResourceUriFetcher.kt -> src/vendor/android/kotlin/coil3/fetch/ResourceUriFetcher.kt
# | androidMain/kotlin/coil3/imageLoaders.android.kt -> src/vendor/android/kotlin/coil3/imageLoaders.android.kt
# | androidMain/kotlin/coil3/intercept/EngineInterceptor.android.kt -> src/vendor/android/kotlin/coil3/intercept/EngineInterceptor.android.kt
# | androidMain/kotlin/coil3/key/AndroidResourceUriKeyer.kt -> src/vendor/android/kotlin/coil3/key/AndroidResourceUriKeyer.kt
# | androidMain/kotlin/coil3/map/AndroidUriMapper.kt -> src/vendor/android/kotlin/coil3/map/AndroidUriMapper.kt
# | androidMain/kotlin/coil3/map/ResourceIntMapper.kt -> src/vendor/android/kotlin/coil3/map/ResourceIntMapper.kt
# | androidMain/kotlin/coil3/request/RequestDelegate.android.kt -> src/vendor/android/kotlin/coil3/request/RequestDelegate.android.kt
# | androidMain/kotlin/coil3/request/RequestService.android.kt -> src/vendor/android/kotlin/coil3/request/RequestService.android.kt
# | androidMain/kotlin/coil3/request/ViewTargetDisposable.kt -> src/vendor/android/kotlin/coil3/request/ViewTargetDisposable.kt
# | androidMain/kotlin/coil3/request/ViewTargetRequestManager.kt -> src/vendor/android/kotlin/coil3/request/ViewTargetRequestManager.kt
# | androidMain/kotlin/coil3/request/imageRequests.android.kt -> src/vendor/android/kotlin/coil3/request/imageRequests.android.kt
# | androidMain/kotlin/coil3/size/RealViewSizeResolver.kt -> src/vendor/android/kotlin/coil3/size/RealViewSizeResolver.kt
# | androidMain/kotlin/coil3/size/ScaleDrawable.kt -> src/vendor/android/kotlin/coil3/size/ScaleDrawable.kt
# | androidMain/kotlin/coil3/size/ViewSizeResolver.kt -> src/vendor/android/kotlin/coil3/size/ViewSizeResolver.kt
# | androidMain/kotlin/coil3/target/GenericViewTarget.kt -> src/vendor/android/kotlin/coil3/target/GenericViewTarget.kt
# | androidMain/kotlin/coil3/target/ImageViewTarget.kt -> src/vendor/android/kotlin/coil3/target/ImageViewTarget.kt
# | androidMain/kotlin/coil3/target/ViewTarget.kt -> src/vendor/android/kotlin/coil3/target/ViewTarget.kt
# | androidMain/kotlin/coil3/transform/CircleCropTransformation.kt -> src/vendor/android/kotlin/coil3/transform/CircleCropTransformation.kt
# | androidMain/kotlin/coil3/transform/RoundedCornersTransformation.kt -> src/vendor/android/kotlin/coil3/transform/RoundedCornersTransformation.kt
# | androidMain/kotlin/coil3/transform/transformations.kt -> src/vendor/android/kotlin/coil3/transform/transformations.kt
# | androidMain/kotlin/coil3/transition/CrossfadeDrawable.kt -> src/vendor/android/kotlin/coil3/transition/CrossfadeDrawable.kt
# | androidMain/kotlin/coil3/transition/CrossfadeTransition.kt -> src/vendor/android/kotlin/coil3/transition/CrossfadeTransition.kt
# | androidMain/kotlin/coil3/transition/NoneTransition.kt -> src/vendor/android/kotlin/coil3/transition/NoneTransition.kt
# | androidMain/kotlin/coil3/transition/Transition.kt -> src/vendor/android/kotlin/coil3/transition/Transition.kt
# | androidMain/kotlin/coil3/transition/TransitionTarget.kt -> src/vendor/android/kotlin/coil3/transition/TransitionTarget.kt
# | androidMain/kotlin/coil3/util/CoilUtils.kt -> src/vendor/android/kotlin/coil3/util/CoilUtils.kt
# | androidMain/kotlin/coil3/util/DrawableUtils.kt -> src/vendor/android/kotlin/coil3/util/DrawableUtils.kt
# | androidMain/kotlin/coil3/util/SystemCallbacks.kt -> src/vendor/android/kotlin/coil3/util/SystemCallbacks.kt
# | androidMain/kotlin/coil3/util/bitmaps.kt -> src/vendor/android/kotlin/coil3/util/bitmaps.kt
# | androidMain/kotlin/coil3/util/contexts.kt -> src/vendor/android/kotlin/coil3/util/contexts.kt
# | androidMain/kotlin/coil3/util/fileSystems.android.kt -> src/vendor/android/kotlin/coil3/util/fileSystems.android.kt
# | androidMain/kotlin/coil3/util/hardwareBitmaps.kt -> src/vendor/android/kotlin/coil3/util/hardwareBitmaps.kt
# | androidMain/kotlin/coil3/util/lifecycles.kt -> src/vendor/android/kotlin/coil3/util/lifecycles.kt
# | androidMain/kotlin/coil3/util/mimeTypes.android.kt -> src/vendor/android/kotlin/coil3/util/mimeTypes.android.kt
# | androidMain/kotlin/coil3/util/utils.android.kt -> src/vendor/android/kotlin/coil3/util/utils.android.kt
# jsCommonMain (7)
# | jsCommonMain/kotlin/coil3/decode/SkiaImageDecoder.jsCommon.kt -> src/vendor/jsCommon/kotlin/coil3/decode/SkiaImageDecoder.jsCommon.kt
# | jsCommonMain/kotlin/coil3/decode/WebWorker.kt -> src/vendor/jsCommon/kotlin/coil3/decode/WebWorker.kt
# | jsCommonMain/kotlin/coil3/disk/utils.kt -> src/vendor/jsCommon/kotlin/coil3/disk/utils.kt
# | jsCommonMain/kotlin/coil3/fetch/BlobUriFetcher.kt -> src/vendor/jsCommon/kotlin/coil3/fetch/BlobUriFetcher.kt
# | jsCommonMain/kotlin/coil3/fetch/internal/BlobUriFetcherServiceLoaderTarget.kt -> src/vendor/jsCommon/kotlin/coil3/fetch/internal/BlobUriFetcherServiceLoaderTarget.kt
# | jsCommonMain/kotlin/coil3/util/coroutines.kt -> src/vendor/jsCommon/kotlin/coil3/util/coroutines.kt
# | jsCommonMain/kotlin/coil3/util/fileSystems.kt -> src/vendor/jsCommon/kotlin/coil3/util/fileSystems.kt
# jsMain (3)
# | jsMain/kotlin/coil3/fetch/BlobUriFetcher.js.kt -> src/vendor/js/kotlin/coil3/fetch/BlobUriFetcher.js.kt
# | jsMain/kotlin/coil3/fetch/internal/BlobUriFetcherServiceLoaderTarget.js.kt -> src/vendor/js/kotlin/coil3/fetch/internal/BlobUriFetcherServiceLoaderTarget.js.kt
# | jsMain/kotlin/coil3/util/utils.js.kt -> src/vendor/js/kotlin/coil3/util/utils.js.kt
# jvmCommonMain (10)
# | jvmCommonMain/kotlin/coil3/RealImageLoader.jvmCommon.kt -> src/vendor/jvmCommon/kotlin/coil3/RealImageLoader.jvmCommon.kt
# | jvmCommonMain/kotlin/coil3/decode/ImageSource.kt -> src/vendor/jvmCommon/kotlin/coil3/decode/ImageSource.kt
# | jvmCommonMain/kotlin/coil3/disk/diskCache.kt -> src/vendor/jvmCommon/kotlin/coil3/disk/diskCache.kt
# | jvmCommonMain/kotlin/coil3/fetch/ByteBufferFetcher.kt -> src/vendor/jvmCommon/kotlin/coil3/fetch/ByteBufferFetcher.kt
# | jvmCommonMain/kotlin/coil3/fetch/JarFileFetcher.kt -> src/vendor/jvmCommon/kotlin/coil3/fetch/JarFileFetcher.kt
# | jvmCommonMain/kotlin/coil3/imageLoaders.hidden.kt -> src/vendor/jvmCommon/kotlin/coil3/imageLoaders.hidden.kt
# | jvmCommonMain/kotlin/coil3/map/FileMapper.kt -> src/vendor/jvmCommon/kotlin/coil3/map/FileMapper.kt
# | jvmCommonMain/kotlin/coil3/util/ServiceLoaderComponentRegistry.kt -> src/vendor/jvmCommon/kotlin/coil3/util/ServiceLoaderComponentRegistry.kt
# | jvmCommonMain/kotlin/coil3/util/collections.jvmCommon.kt -> src/vendor/jvmCommon/kotlin/coil3/util/collections.jvmCommon.kt
# | jvmCommonMain/kotlin/coil3/util/utils.jvmCommon.kt -> src/vendor/jvmCommon/kotlin/coil3/util/utils.jvmCommon.kt
# jvmMain (3)
# | jvmMain/kotlin/coil3/decode/SkiaImageDecoder.jvm.kt -> src/vendor/jvm/kotlin/coil3/decode/SkiaImageDecoder.jvm.kt
# | jvmMain/kotlin/coil3/util/contexts.jvm.kt -> src/vendor/jvm/kotlin/coil3/util/contexts.jvm.kt
# | jvmMain/kotlin/coil3/util/fileSystems.jvm.kt -> src/vendor/jvm/kotlin/coil3/util/fileSystems.jvm.kt
# wasmJsMain (3)
# | wasmJsMain/kotlin/coil3/fetch/BlobUriFetcher.wasmJs.kt -> src/vendor/wasmJs/kotlin/coil3/fetch/BlobUriFetcher.wasmJs.kt
# | wasmJsMain/kotlin/coil3/fetch/internal/BlobUriFetcherServiceLoaderTarget.wasmJs.kt -> src/vendor/wasmJs/kotlin/coil3/fetch/internal/BlobUriFetcherServiceLoaderTarget.wasmJs.kt
# | wasmJsMain/kotlin/coil3/util/utils.wasmJs.kt -> src/vendor/wasmJs/kotlin/coil3/util/utils.wasmJs.kt
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
@@ -0,0 +1,197 @@
// Klib ABI Dump
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.coil3:coil-network-core>
abstract fun interface coil3.network/ConnectivityChecker { // coil3.network/ConnectivityChecker|null[0]
abstract fun isOnline(): kotlin/Boolean // coil3.network/ConnectivityChecker.isOnline|isOnline(){}[0]
final object Companion { // coil3.network/ConnectivityChecker.Companion|null[0]
final val ONLINE // coil3.network/ConnectivityChecker.Companion.ONLINE|{}ONLINE[0]
final fun <get-ONLINE>(): coil3.network/ConnectivityChecker // coil3.network/ConnectivityChecker.Companion.ONLINE.<get-ONLINE>|<get-ONLINE>(){}[0]
}
}
abstract interface coil3.network/CacheStrategy { // coil3.network/CacheStrategy|null[0]
abstract suspend fun read(coil3.network/NetworkResponse, coil3.network/NetworkRequest, coil3.request/Options): coil3.network/CacheStrategy.ReadResult // coil3.network/CacheStrategy.read|read(coil3.network.NetworkResponse;coil3.network.NetworkRequest;coil3.request.Options){}[0]
abstract suspend fun write(coil3.network/NetworkResponse?, coil3.network/NetworkRequest, coil3.network/NetworkResponse, coil3.request/Options): coil3.network/CacheStrategy.WriteResult // coil3.network/CacheStrategy.write|write(coil3.network.NetworkResponse?;coil3.network.NetworkRequest;coil3.network.NetworkResponse;coil3.request.Options){}[0]
final class ReadResult { // coil3.network/CacheStrategy.ReadResult|null[0]
constructor <init>(coil3.network/NetworkRequest) // coil3.network/CacheStrategy.ReadResult.<init>|<init>(coil3.network.NetworkRequest){}[0]
constructor <init>(coil3.network/NetworkResponse) // coil3.network/CacheStrategy.ReadResult.<init>|<init>(coil3.network.NetworkResponse){}[0]
final val request // coil3.network/CacheStrategy.ReadResult.request|{}request[0]
final fun <get-request>(): coil3.network/NetworkRequest? // coil3.network/CacheStrategy.ReadResult.request.<get-request>|<get-request>(){}[0]
final val response // coil3.network/CacheStrategy.ReadResult.response|{}response[0]
final fun <get-response>(): coil3.network/NetworkResponse? // coil3.network/CacheStrategy.ReadResult.response.<get-response>|<get-response>(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.network/CacheStrategy.ReadResult.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // coil3.network/CacheStrategy.ReadResult.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // coil3.network/CacheStrategy.ReadResult.toString|toString(){}[0]
}
final class WriteResult { // coil3.network/CacheStrategy.WriteResult|null[0]
constructor <init>(coil3.network/NetworkResponse) // coil3.network/CacheStrategy.WriteResult.<init>|<init>(coil3.network.NetworkResponse){}[0]
final val response // coil3.network/CacheStrategy.WriteResult.response|{}response[0]
final fun <get-response>(): coil3.network/NetworkResponse? // coil3.network/CacheStrategy.WriteResult.response.<get-response>|<get-response>(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.network/CacheStrategy.WriteResult.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // coil3.network/CacheStrategy.WriteResult.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // coil3.network/CacheStrategy.WriteResult.toString|toString(){}[0]
final object Companion { // coil3.network/CacheStrategy.WriteResult.Companion|null[0]
final val DISABLED // coil3.network/CacheStrategy.WriteResult.Companion.DISABLED|{}DISABLED[0]
final fun <get-DISABLED>(): coil3.network/CacheStrategy.WriteResult // coil3.network/CacheStrategy.WriteResult.Companion.DISABLED.<get-DISABLED>|<get-DISABLED>(){}[0]
}
}
final object Companion { // coil3.network/CacheStrategy.Companion|null[0]
final val DEFAULT // coil3.network/CacheStrategy.Companion.DEFAULT|{}DEFAULT[0]
final fun <get-DEFAULT>(): coil3.network/CacheStrategy // coil3.network/CacheStrategy.Companion.DEFAULT.<get-DEFAULT>|<get-DEFAULT>(){}[0]
}
}
abstract interface coil3.network/ConcurrentRequestStrategy { // coil3.network/ConcurrentRequestStrategy|null[0]
abstract suspend fun apply(kotlin/String, kotlin.coroutines/SuspendFunction0<coil3.fetch/FetchResult>): coil3.fetch/FetchResult // coil3.network/ConcurrentRequestStrategy.apply|apply(kotlin.String;kotlin.coroutines.SuspendFunction0<coil3.fetch.FetchResult>){}[0]
final object Companion { // coil3.network/ConcurrentRequestStrategy.Companion|null[0]
final val UNCOORDINATED // coil3.network/ConcurrentRequestStrategy.Companion.UNCOORDINATED|{}UNCOORDINATED[0]
final fun <get-UNCOORDINATED>(): coil3.network/ConcurrentRequestStrategy // coil3.network/ConcurrentRequestStrategy.Companion.UNCOORDINATED.<get-UNCOORDINATED>|<get-UNCOORDINATED>(){}[0]
}
}
abstract interface coil3.network/NetworkClient { // coil3.network/NetworkClient|null[0]
abstract suspend fun <#A1: kotlin/Any?> executeRequest(coil3.network/NetworkRequest, kotlin.coroutines/SuspendFunction1<coil3.network/NetworkResponse, #A1>): #A1 // coil3.network/NetworkClient.executeRequest|executeRequest(coil3.network.NetworkRequest;kotlin.coroutines.SuspendFunction1<coil3.network.NetworkResponse,0:0>){0§<kotlin.Any?>}[0]
}
abstract interface coil3.network/NetworkRequestBody { // coil3.network/NetworkRequestBody|null[0]
abstract suspend fun writeTo(okio/BufferedSink) // coil3.network/NetworkRequestBody.writeTo|writeTo(okio.BufferedSink){}[0]
}
abstract interface coil3.network/NetworkResponseBody : kotlin/AutoCloseable { // coil3.network/NetworkResponseBody|null[0]
abstract suspend fun writeTo(okio/BufferedSink) // coil3.network/NetworkResponseBody.writeTo|writeTo(okio.BufferedSink){}[0]
abstract suspend fun writeTo(okio/FileSystem, okio/Path) // coil3.network/NetworkResponseBody.writeTo|writeTo(okio.FileSystem;okio.Path){}[0]
}
final class coil3.network/DeDupeConcurrentRequestStrategy : coil3.network/ConcurrentRequestStrategy { // coil3.network/DeDupeConcurrentRequestStrategy|null[0]
constructor <init>() // coil3.network/DeDupeConcurrentRequestStrategy.<init>|<init>(){}[0]
final suspend fun apply(kotlin/String, kotlin.coroutines/SuspendFunction0<coil3.fetch/FetchResult>): coil3.fetch/FetchResult // coil3.network/DeDupeConcurrentRequestStrategy.apply|apply(kotlin.String;kotlin.coroutines.SuspendFunction0<coil3.fetch.FetchResult>){}[0]
}
final class coil3.network/HttpException : kotlin/RuntimeException { // coil3.network/HttpException|null[0]
constructor <init>(coil3.network/NetworkResponse) // coil3.network/HttpException.<init>|<init>(coil3.network.NetworkResponse){}[0]
final val response // coil3.network/HttpException.response|{}response[0]
final fun <get-response>(): coil3.network/NetworkResponse // coil3.network/HttpException.response.<get-response>|<get-response>(){}[0]
}
final class coil3.network/NetworkFetcher : coil3.fetch/Fetcher { // coil3.network/NetworkFetcher|null[0]
constructor <init>(kotlin/String, coil3.request/Options, kotlin/Lazy<coil3.network/NetworkClient>, kotlin/Lazy<coil3.disk/DiskCache?>, kotlin/Lazy<coil3.network/CacheStrategy>, coil3.network/ConnectivityChecker) // coil3.network/NetworkFetcher.<init>|<init>(kotlin.String;coil3.request.Options;kotlin.Lazy<coil3.network.NetworkClient>;kotlin.Lazy<coil3.disk.DiskCache?>;kotlin.Lazy<coil3.network.CacheStrategy>;coil3.network.ConnectivityChecker){}[0]
constructor <init>(kotlin/String, coil3.request/Options, kotlin/Lazy<coil3.network/NetworkClient>, kotlin/Lazy<coil3.disk/DiskCache?>, kotlin/Lazy<coil3.network/CacheStrategy>, kotlin/Lazy<coil3.network/ConnectivityChecker>, kotlin/Lazy<coil3.network/ConcurrentRequestStrategy>) // coil3.network/NetworkFetcher.<init>|<init>(kotlin.String;coil3.request.Options;kotlin.Lazy<coil3.network.NetworkClient>;kotlin.Lazy<coil3.disk.DiskCache?>;kotlin.Lazy<coil3.network.CacheStrategy>;kotlin.Lazy<coil3.network.ConnectivityChecker>;kotlin.Lazy<coil3.network.ConcurrentRequestStrategy>){}[0]
final fun getMimeType(kotlin/String, kotlin/String?): kotlin/String? // coil3.network/NetworkFetcher.getMimeType|getMimeType(kotlin.String;kotlin.String?){}[0]
final suspend fun fetch(): coil3.fetch/FetchResult // coil3.network/NetworkFetcher.fetch|fetch(){}[0]
final class Factory : coil3.fetch/Fetcher.Factory<coil3/Uri> { // coil3.network/NetworkFetcher.Factory|null[0]
constructor <init>(kotlin/Function0<coil3.network/NetworkClient>, kotlin/Function0<coil3.network/CacheStrategy> = ..., kotlin/Function1<coil3/PlatformContext, coil3.network/ConnectivityChecker> = ...) // coil3.network/NetworkFetcher.Factory.<init>|<init>(kotlin.Function0<coil3.network.NetworkClient>;kotlin.Function0<coil3.network.CacheStrategy>;kotlin.Function1<coil3.PlatformContext,coil3.network.ConnectivityChecker>){}[0]
constructor <init>(kotlin/Function0<coil3.network/NetworkClient>, kotlin/Function0<coil3.network/CacheStrategy> = ..., kotlin/Function1<coil3/PlatformContext, coil3.network/ConnectivityChecker> = ..., kotlin/Function0<coil3.network/ConcurrentRequestStrategy> = ...) // coil3.network/NetworkFetcher.Factory.<init>|<init>(kotlin.Function0<coil3.network.NetworkClient>;kotlin.Function0<coil3.network.CacheStrategy>;kotlin.Function1<coil3.PlatformContext,coil3.network.ConnectivityChecker>;kotlin.Function0<coil3.network.ConcurrentRequestStrategy>){}[0]
final fun create(coil3/Uri, coil3.request/Options, coil3/ImageLoader): coil3.fetch/Fetcher? // coil3.network/NetworkFetcher.Factory.create|create(coil3.Uri;coil3.request.Options;coil3.ImageLoader){}[0]
}
}
final class coil3.network/NetworkHeaders { // coil3.network/NetworkHeaders|null[0]
final fun asMap(): kotlin.collections/Map<kotlin/String, kotlin.collections/List<kotlin/String>> // coil3.network/NetworkHeaders.asMap|asMap(){}[0]
final fun get(kotlin/String): kotlin/String? // coil3.network/NetworkHeaders.get|get(kotlin.String){}[0]
final fun getAll(kotlin/String): kotlin.collections/List<kotlin/String> // coil3.network/NetworkHeaders.getAll|getAll(kotlin.String){}[0]
final fun newBuilder(): coil3.network/NetworkHeaders.Builder // coil3.network/NetworkHeaders.newBuilder|newBuilder(){}[0]
final class Builder { // coil3.network/NetworkHeaders.Builder|null[0]
constructor <init>() // coil3.network/NetworkHeaders.Builder.<init>|<init>(){}[0]
constructor <init>(coil3.network/NetworkHeaders) // coil3.network/NetworkHeaders.Builder.<init>|<init>(coil3.network.NetworkHeaders){}[0]
final fun add(kotlin/String, kotlin/String): coil3.network/NetworkHeaders.Builder // coil3.network/NetworkHeaders.Builder.add|add(kotlin.String;kotlin.String){}[0]
final fun build(): coil3.network/NetworkHeaders // coil3.network/NetworkHeaders.Builder.build|build(){}[0]
final fun set(kotlin/String, kotlin.collections/List<kotlin/String>): coil3.network/NetworkHeaders.Builder // coil3.network/NetworkHeaders.Builder.set|set(kotlin.String;kotlin.collections.List<kotlin.String>){}[0]
final fun set(kotlin/String, kotlin/String): coil3.network/NetworkHeaders.Builder // coil3.network/NetworkHeaders.Builder.set|set(kotlin.String;kotlin.String){}[0]
}
final object Companion { // coil3.network/NetworkHeaders.Companion|null[0]
final val EMPTY // coil3.network/NetworkHeaders.Companion.EMPTY|{}EMPTY[0]
final fun <get-EMPTY>(): coil3.network/NetworkHeaders // coil3.network/NetworkHeaders.Companion.EMPTY.<get-EMPTY>|<get-EMPTY>(){}[0]
}
}
final class coil3.network/NetworkRequest { // coil3.network/NetworkRequest|null[0]
constructor <init>(kotlin/String, kotlin/String = ..., coil3.network/NetworkHeaders = ..., coil3.network/NetworkRequestBody? = ...) // coil3.network/NetworkRequest.<init>|<init>(kotlin.String;kotlin.String;coil3.network.NetworkHeaders;coil3.network.NetworkRequestBody?){}[0]
constructor <init>(kotlin/String, kotlin/String = ..., coil3.network/NetworkHeaders = ..., coil3.network/NetworkRequestBody? = ..., coil3/Extras = ...) // coil3.network/NetworkRequest.<init>|<init>(kotlin.String;kotlin.String;coil3.network.NetworkHeaders;coil3.network.NetworkRequestBody?;coil3.Extras){}[0]
final val body // coil3.network/NetworkRequest.body|{}body[0]
final fun <get-body>(): coil3.network/NetworkRequestBody? // coil3.network/NetworkRequest.body.<get-body>|<get-body>(){}[0]
final val extras // coil3.network/NetworkRequest.extras|{}extras[0]
final fun <get-extras>(): coil3/Extras // coil3.network/NetworkRequest.extras.<get-extras>|<get-extras>(){}[0]
final val headers // coil3.network/NetworkRequest.headers|{}headers[0]
final fun <get-headers>(): coil3.network/NetworkHeaders // coil3.network/NetworkRequest.headers.<get-headers>|<get-headers>(){}[0]
final val method // coil3.network/NetworkRequest.method|{}method[0]
final fun <get-method>(): kotlin/String // coil3.network/NetworkRequest.method.<get-method>|<get-method>(){}[0]
final val url // coil3.network/NetworkRequest.url|{}url[0]
final fun <get-url>(): kotlin/String // coil3.network/NetworkRequest.url.<get-url>|<get-url>(){}[0]
final fun copy(kotlin/String = ..., kotlin/String = ..., coil3.network/NetworkHeaders = ..., coil3.network/NetworkRequestBody? = ...): coil3.network/NetworkRequest // coil3.network/NetworkRequest.copy|copy(kotlin.String;kotlin.String;coil3.network.NetworkHeaders;coil3.network.NetworkRequestBody?){}[0]
final fun copy(kotlin/String = ..., kotlin/String = ..., coil3.network/NetworkHeaders = ..., coil3.network/NetworkRequestBody? = ..., coil3/Extras = ...): coil3.network/NetworkRequest // coil3.network/NetworkRequest.copy|copy(kotlin.String;kotlin.String;coil3.network.NetworkHeaders;coil3.network.NetworkRequestBody?;coil3.Extras){}[0]
}
final class coil3.network/NetworkResponse { // coil3.network/NetworkResponse|null[0]
constructor <init>(kotlin/Int = ..., kotlin/Long = ..., kotlin/Long = ..., coil3.network/NetworkHeaders = ..., coil3.network/NetworkResponseBody? = ..., kotlin/Any? = ...) // coil3.network/NetworkResponse.<init>|<init>(kotlin.Int;kotlin.Long;kotlin.Long;coil3.network.NetworkHeaders;coil3.network.NetworkResponseBody?;kotlin.Any?){}[0]
final val body // coil3.network/NetworkResponse.body|{}body[0]
final fun <get-body>(): coil3.network/NetworkResponseBody? // coil3.network/NetworkResponse.body.<get-body>|<get-body>(){}[0]
final val code // coil3.network/NetworkResponse.code|{}code[0]
final fun <get-code>(): kotlin/Int // coil3.network/NetworkResponse.code.<get-code>|<get-code>(){}[0]
final val delegate // coil3.network/NetworkResponse.delegate|{}delegate[0]
final fun <get-delegate>(): kotlin/Any? // coil3.network/NetworkResponse.delegate.<get-delegate>|<get-delegate>(){}[0]
final val headers // coil3.network/NetworkResponse.headers|{}headers[0]
final fun <get-headers>(): coil3.network/NetworkHeaders // coil3.network/NetworkResponse.headers.<get-headers>|<get-headers>(){}[0]
final val requestMillis // coil3.network/NetworkResponse.requestMillis|{}requestMillis[0]
final fun <get-requestMillis>(): kotlin/Long // coil3.network/NetworkResponse.requestMillis.<get-requestMillis>|<get-requestMillis>(){}[0]
final val responseMillis // coil3.network/NetworkResponse.responseMillis|{}responseMillis[0]
final fun <get-responseMillis>(): kotlin/Long // coil3.network/NetworkResponse.responseMillis.<get-responseMillis>|<get-responseMillis>(){}[0]
final fun copy(kotlin/Int = ..., kotlin/Long = ..., kotlin/Long = ..., coil3.network/NetworkHeaders = ..., coil3.network/NetworkResponseBody? = ..., kotlin/Any? = ...): coil3.network/NetworkResponse // coil3.network/NetworkResponse.copy|copy(kotlin.Int;kotlin.Long;kotlin.Long;coil3.network.NetworkHeaders;coil3.network.NetworkResponseBody?;kotlin.Any?){}[0]
}
final object coil3.network/CacheNetworkResponse { // coil3.network/CacheNetworkResponse|null[0]
final fun readFrom(okio/BufferedSource): coil3.network/NetworkResponse // coil3.network/CacheNetworkResponse.readFrom|readFrom(okio.BufferedSource){}[0]
final fun writeTo(coil3.network/NetworkResponse, okio/BufferedSink) // coil3.network/CacheNetworkResponse.writeTo|writeTo(coil3.network.NetworkResponse;okio.BufferedSink){}[0]
}
final val coil3.network/httpBody // coil3.network/httpBody|@coil3.Extras.Key.Companion{}httpBody[0]
final fun (coil3/Extras.Key.Companion).<get-httpBody>(): coil3/Extras.Key<coil3.network/NetworkRequestBody?> // coil3.network/httpBody.<get-httpBody>|<get-httpBody>@coil3.Extras.Key.Companion(){}[0]
final val coil3.network/httpBody // coil3.network/httpBody|@coil3.request.ImageRequest{}httpBody[0]
final fun (coil3.request/ImageRequest).<get-httpBody>(): coil3.network/NetworkRequestBody? // coil3.network/httpBody.<get-httpBody>|<get-httpBody>@coil3.request.ImageRequest(){}[0]
final val coil3.network/httpBody // coil3.network/httpBody|@coil3.request.Options{}httpBody[0]
final fun (coil3.request/Options).<get-httpBody>(): coil3.network/NetworkRequestBody? // coil3.network/httpBody.<get-httpBody>|<get-httpBody>@coil3.request.Options(){}[0]
final val coil3.network/httpHeaders // coil3.network/httpHeaders|@coil3.Extras.Key.Companion{}httpHeaders[0]
final fun (coil3/Extras.Key.Companion).<get-httpHeaders>(): coil3/Extras.Key<coil3.network/NetworkHeaders> // coil3.network/httpHeaders.<get-httpHeaders>|<get-httpHeaders>@coil3.Extras.Key.Companion(){}[0]
final val coil3.network/httpHeaders // coil3.network/httpHeaders|@coil3.request.ImageRequest{}httpHeaders[0]
final fun (coil3.request/ImageRequest).<get-httpHeaders>(): coil3.network/NetworkHeaders // coil3.network/httpHeaders.<get-httpHeaders>|<get-httpHeaders>@coil3.request.ImageRequest(){}[0]
final val coil3.network/httpHeaders // coil3.network/httpHeaders|@coil3.request.Options{}httpHeaders[0]
final fun (coil3.request/Options).<get-httpHeaders>(): coil3.network/NetworkHeaders // coil3.network/httpHeaders.<get-httpHeaders>|<get-httpHeaders>@coil3.request.Options(){}[0]
final val coil3.network/httpMethod // coil3.network/httpMethod|@coil3.Extras.Key.Companion{}httpMethod[0]
final fun (coil3/Extras.Key.Companion).<get-httpMethod>(): coil3/Extras.Key<kotlin/String> // coil3.network/httpMethod.<get-httpMethod>|<get-httpMethod>@coil3.Extras.Key.Companion(){}[0]
final val coil3.network/httpMethod // coil3.network/httpMethod|@coil3.request.ImageRequest{}httpMethod[0]
final fun (coil3.request/ImageRequest).<get-httpMethod>(): kotlin/String // coil3.network/httpMethod.<get-httpMethod>|<get-httpMethod>@coil3.request.ImageRequest(){}[0]
final val coil3.network/httpMethod // coil3.network/httpMethod|@coil3.request.Options{}httpMethod[0]
final fun (coil3.request/Options).<get-httpMethod>(): kotlin/String // coil3.network/httpMethod.<get-httpMethod>|<get-httpMethod>@coil3.request.Options(){}[0]
final fun (coil3.request/ImageRequest.Builder).coil3.network/httpBody(coil3.network/NetworkRequestBody): coil3.request/ImageRequest.Builder // coil3.network/httpBody|httpBody@coil3.request.ImageRequest.Builder(coil3.network.NetworkRequestBody){}[0]
final fun (coil3.request/ImageRequest.Builder).coil3.network/httpHeaders(coil3.network/NetworkHeaders): coil3.request/ImageRequest.Builder // coil3.network/httpHeaders|httpHeaders@coil3.request.ImageRequest.Builder(coil3.network.NetworkHeaders){}[0]
final fun (coil3.request/ImageRequest.Builder).coil3.network/httpMethod(kotlin/String): coil3.request/ImageRequest.Builder // coil3.network/httpMethod|httpMethod@coil3.request.ImageRequest.Builder(kotlin.String){}[0]
final fun coil3.network/ConnectivityChecker(coil3/PlatformContext): coil3.network/ConnectivityChecker // coil3.network/ConnectivityChecker|ConnectivityChecker(coil3.PlatformContext){}[0]
final fun coil3.network/NetworkRequestBody(okio/ByteString): coil3.network/NetworkRequestBody // coil3.network/NetworkRequestBody|NetworkRequestBody(okio.ByteString){}[0]
final fun coil3.network/NetworkResponseBody(okio/BufferedSource): coil3.network/NetworkResponseBody // coil3.network/NetworkResponseBody|NetworkResponseBody(okio.BufferedSource){}[0]
+34
View File
@@ -0,0 +1,34 @@
// :coil:coil-network-core - vendored VERBATIM from coil-kt/coil into src/vendor.
// Shared network-fetcher plumbing. Upstream: linux + apple, no mingwX64.
// Upstream's non* source sets sit ABOVE nativeMain here (see coil-core's build file
// and compose-fork.txt for why). Provenance = compose-fork.txt; never hand-edit
// src/vendor - change the manifest + `python scripts/compose-fork/sync.py coil/coil-network-core`.
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
kotlin {
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
applyDefaultHierarchyTemplate()
sourceSets {
commonMain {
dependencies { api(project(":coil:coil-core")) }
kotlin.srcDir("src/vendor/common/kotlin")
}
val vNonAndroidMain = create("coilNonAndroidMain") {
dependsOn(commonMain.get())
kotlin.srcDir("src/vendor/nonAndroid/kotlin")
}
nativeMain { dependsOn(vNonAndroidMain) }
}
compilerOptions {
freeCompilerArgs.addAll(
"-Xexpect-actual-classes",
"-opt-in=coil3.annotation.InternalCoilApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=coil3.annotation.DelicateCoilApi",
)
}
}
+37
View File
@@ -0,0 +1,37 @@
# :coil:coil-network-core - shared network fetcher plumbing.
# Upstream: linux + apple, no mingwX64.
# Upstream source sets collapse onto this port: it has NO android, jvm or js
# target, so nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all
# apply to EVERY target here. They land in TWO trees, not one: the non* sets keep
# their own level (src/vendor/nonAndroid) ABOVE src/vendor/native, because some of
# them declare `expect`s whose `actual`s live in upstream nativeMain - collapsing
# both into one source set makes expect and actual siblings, which is an error. appleMain is macosArm64
# only and nonAppleMain is linux + mingw, so those two keep their own trees (the
# build file wires a `nonAppleMain` intermediate; appleMain comes from the default
# hierarchy template).
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
SET_FOLDER=coil-network-core/src
commonMain/kotlin/ -> src/vendor/common/kotlin/
# | commonMain/kotlin/coil3/network/CacheNetworkResponse.kt -> src/vendor/common/kotlin/coil3/network/CacheNetworkResponse.kt
# | commonMain/kotlin/coil3/network/CacheStrategy.kt -> src/vendor/common/kotlin/coil3/network/CacheStrategy.kt
# | commonMain/kotlin/coil3/network/ConcurrentRequestStrategy.kt -> src/vendor/common/kotlin/coil3/network/ConcurrentRequestStrategy.kt
# | commonMain/kotlin/coil3/network/ConnectivityChecker.kt -> src/vendor/common/kotlin/coil3/network/ConnectivityChecker.kt
# | commonMain/kotlin/coil3/network/HttpException.kt -> src/vendor/common/kotlin/coil3/network/HttpException.kt
# | commonMain/kotlin/coil3/network/NetworkClient.kt -> src/vendor/common/kotlin/coil3/network/NetworkClient.kt
# | commonMain/kotlin/coil3/network/NetworkFetcher.kt -> src/vendor/common/kotlin/coil3/network/NetworkFetcher.kt
# | commonMain/kotlin/coil3/network/NetworkHeaders.kt -> src/vendor/common/kotlin/coil3/network/NetworkHeaders.kt
# | commonMain/kotlin/coil3/network/imageRequests.kt -> src/vendor/common/kotlin/coil3/network/imageRequests.kt
# | commonMain/kotlin/coil3/network/internal/DefaultCacheStrategy.kt -> src/vendor/common/kotlin/coil3/network/internal/DefaultCacheStrategy.kt
# | commonMain/kotlin/coil3/network/internal/SingleParameterLazy.kt -> src/vendor/common/kotlin/coil3/network/internal/SingleParameterLazy.kt
# | commonMain/kotlin/coil3/network/internal/utils.kt -> src/vendor/common/kotlin/coil3/network/internal/utils.kt
nonAndroidMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
# | nonAndroidMain/kotlin/coil3/network/ConnectivityChecker.kt -> src/vendor/nonAndroid/kotlin/coil3/network/ConnectivityChecker.kt
# | nonAndroidMain/kotlin/coil3/network/internal/utils.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/network/internal/utils.nonAndroid.kt
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
# androidMain (2)
# | androidMain/kotlin/coil3/network/ConnectivityChecker.kt -> src/vendor/android/kotlin/coil3/network/ConnectivityChecker.kt
# | androidMain/kotlin/coil3/network/internal/utils.android.kt -> src/vendor/android/kotlin/coil3/network/internal/utils.android.kt
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
@@ -0,0 +1,18 @@
// Klib ABI Dump
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.coil3:coil-network-ktor3>
final val coil3.network.ktor3.internal/initHook // coil3.network.ktor3.internal/initHook|{}initHook[0]
final fun <get-initHook>(): kotlin/Any // coil3.network.ktor3.internal/initHook.<get-initHook>|<get-initHook>(){}[0]
final fun (io.ktor.client/HttpClient).coil3.network.ktor3/asNetworkClient(): coil3.network/NetworkClient // coil3.network.ktor3/asNetworkClient|asNetworkClient@io.ktor.client.HttpClient(){}[0]
final fun coil3.network.ktor3/KtorNetworkFetcherFactory(): coil3.network/NetworkFetcher.Factory // coil3.network.ktor3/KtorNetworkFetcherFactory|KtorNetworkFetcherFactory(){}[0]
final fun coil3.network.ktor3/KtorNetworkFetcherFactory(io.ktor.client/HttpClient): coil3.network/NetworkFetcher.Factory // coil3.network.ktor3/KtorNetworkFetcherFactory|KtorNetworkFetcherFactory(io.ktor.client.HttpClient){}[0]
final fun coil3.network.ktor3/KtorNetworkFetcherFactory(io.ktor.client/HttpClient, coil3.network/ConcurrentRequestStrategy = ...): coil3.network/NetworkFetcher.Factory // coil3.network.ktor3/KtorNetworkFetcherFactory|KtorNetworkFetcherFactory(io.ktor.client.HttpClient;coil3.network.ConcurrentRequestStrategy){}[0]
final fun coil3.network.ktor3/KtorNetworkFetcherFactory(kotlin/Function0<io.ktor.client/HttpClient> = ..., kotlin/Function0<coil3.network/CacheStrategy> = ..., kotlin/Function1<coil3/PlatformContext, coil3.network/ConnectivityChecker> = ...): coil3.network/NetworkFetcher.Factory // coil3.network.ktor3/KtorNetworkFetcherFactory|KtorNetworkFetcherFactory(kotlin.Function0<io.ktor.client.HttpClient>;kotlin.Function0<coil3.network.CacheStrategy>;kotlin.Function1<coil3.PlatformContext,coil3.network.ConnectivityChecker>){}[0]
final fun coil3.network.ktor3/KtorNetworkFetcherFactory(kotlin/Function0<io.ktor.client/HttpClient> = ..., kotlin/Function0<coil3.network/CacheStrategy> = ..., kotlin/Function1<coil3/PlatformContext, coil3.network/ConnectivityChecker> = ..., kotlin/Function0<coil3.network/ConcurrentRequestStrategy> = ...): coil3.network/NetworkFetcher.Factory // coil3.network.ktor3/KtorNetworkFetcherFactory|KtorNetworkFetcherFactory(kotlin.Function0<io.ktor.client.HttpClient>;kotlin.Function0<coil3.network.CacheStrategy>;kotlin.Function1<coil3.PlatformContext,coil3.network.ConnectivityChecker>;kotlin.Function0<coil3.network.ConcurrentRequestStrategy>){}[0]
final fun coil3.network.ktor3/KtorNetworkFetcherFactory(kotlin/Function0<io.ktor.client/HttpClient>): coil3.network/NetworkFetcher.Factory // coil3.network.ktor3/KtorNetworkFetcherFactory|KtorNetworkFetcherFactory(kotlin.Function0<io.ktor.client.HttpClient>){}[0]
+42
View File
@@ -0,0 +1,42 @@
// :coil:coil-network-ktor3 - vendored VERBATIM from coil-kt/coil into src/vendor.
// Ktor 3 network fetcher, on the port's existing ktor pin.
// Upstream's non* source sets sit ABOVE nativeMain here (see coil-core's build file
// and compose-fork.txt for why). Provenance = compose-fork.txt; never hand-edit
// src/vendor - change the manifest + `python scripts/compose-fork/sync.py coil/coil-network-ktor3`.
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
kotlin {
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
applyDefaultHierarchyTemplate()
sourceSets {
commonMain {
dependencies {
api(project(":coil:coil-core"))
api(project(":coil:coil-network-core"))
api(libs.ktor.client.core)
}
kotlin.srcDir("src/vendor/common/kotlin")
}
val vNonAndroidMain = create("coilNonAndroidMain") {
dependsOn(commonMain.get())
kotlin.srcDir("src/vendor/nonAndroid/kotlin")
dependencies { implementation(libs.kotlinx.io.okio) }
}
nativeMain {
dependsOn(vNonAndroidMain)
kotlin.srcDir("src/vendor/native/kotlin")
}
}
compilerOptions {
freeCompilerArgs.addAll(
"-Xexpect-actual-classes",
"-opt-in=coil3.annotation.InternalCoilApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=coil3.annotation.DelicateCoilApi",
)
}
}
+32
View File
@@ -0,0 +1,32 @@
# :coil:coil-network-ktor3 - Ktor 3 network fetcher. Upstream: linux + apple, no mingwX64.
# Pairs with the port's existing ktor pin (the `ktor` entry in libs.versions.toml).
# Upstream source sets collapse onto this port: it has NO android, jvm or js
# target, so nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all
# apply to EVERY target here. They land in TWO trees, not one: the non* sets keep
# their own level (src/vendor/nonAndroid) ABOVE src/vendor/native, because some of
# them declare `expect`s whose `actual`s live in upstream nativeMain - collapsing
# both into one source set makes expect and actual siblings, which is an error. appleMain is macosArm64
# only and nonAppleMain is linux + mingw, so those two keep their own trees (the
# build file wires a `nonAppleMain` intermediate; appleMain comes from the default
# hierarchy template).
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
SET_FOLDER=coil-network-ktor3/src
commonMain/kotlin/ -> src/vendor/common/kotlin/
# | commonMain/kotlin/coil3/network/ktor3/KtorNetworkFetcher.kt -> src/vendor/common/kotlin/coil3/network/ktor3/KtorNetworkFetcher.kt
# | commonMain/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.kt -> src/vendor/common/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.kt
# | commonMain/kotlin/coil3/network/ktor3/internal/utils.kt -> src/vendor/common/kotlin/coil3/network/ktor3/internal/utils.kt
nonJvmCommonMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
# | nonJvmCommonMain/kotlin/coil3/network/ktor3/internal/utils.nonJvmCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/network/ktor3/internal/utils.nonJvmCommon.kt
nativeMain/kotlin/ -> src/vendor/native/kotlin/
# | nativeMain/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.native.kt -> src/vendor/native/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.native.kt
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
# jsMain (1)
# | jsMain/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.js.kt -> src/vendor/js/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.js.kt
# jvmCommonMain (1)
# | jvmCommonMain/kotlin/coil3/network/ktor3/internal/utils.jvmCommon.kt -> src/vendor/jvmCommon/kotlin/coil3/network/ktor3/internal/utils.jvmCommon.kt
# wasmJsMain (1)
# | wasmJsMain/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.wasmJs.kt -> src/vendor/wasmJs/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.wasmJs.kt
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
+123
View File
@@ -0,0 +1,123 @@
// Klib ABI Dump
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.coil3:coil-svg>
abstract interface coil3.svg/Svg { // coil3.svg/Svg|null[0]
abstract val height // coil3.svg/Svg.height|{}height[0]
abstract fun <get-height>(): kotlin/Float // coil3.svg/Svg.height.<get-height>|<get-height>(){}[0]
abstract val width // coil3.svg/Svg.width|{}width[0]
abstract fun <get-width>(): kotlin/Float // coil3.svg/Svg.width.<get-width>|<get-width>(){}[0]
abstract var viewBox // coil3.svg/Svg.viewBox|{}viewBox[0]
abstract fun <get-viewBox>(): coil3.svg/Svg.ViewBox? // coil3.svg/Svg.viewBox.<get-viewBox>|<get-viewBox>(){}[0]
abstract fun <set-viewBox>(coil3.svg/Svg.ViewBox?) // coil3.svg/Svg.viewBox.<set-viewBox>|<set-viewBox>(coil3.svg.Svg.ViewBox?){}[0]
abstract fun asImage(kotlin/Int = ..., kotlin/Int = ...): coil3/Image // coil3.svg/Svg.asImage|asImage(kotlin.Int;kotlin.Int){}[0]
abstract fun height(kotlin/String) // coil3.svg/Svg.height|height(kotlin.String){}[0]
abstract fun options(coil3.request/Options) // coil3.svg/Svg.options|options(coil3.request.Options){}[0]
abstract fun width(kotlin/String) // coil3.svg/Svg.width|width(kotlin.String){}[0]
abstract fun interface Parser { // coil3.svg/Svg.Parser|null[0]
abstract fun parse(okio/BufferedSource): coil3.svg/Svg // coil3.svg/Svg.Parser.parse|parse(okio.BufferedSource){}[0]
final object Companion { // coil3.svg/Svg.Parser.Companion|null[0]
final val DEFAULT // coil3.svg/Svg.Parser.Companion.DEFAULT|{}DEFAULT[0]
final fun <get-DEFAULT>(): coil3.svg/Svg.Parser // coil3.svg/Svg.Parser.Companion.DEFAULT.<get-DEFAULT>|<get-DEFAULT>(){}[0]
}
}
final class ViewBox { // coil3.svg/Svg.ViewBox|null[0]
constructor <init>(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float) // coil3.svg/Svg.ViewBox.<init>|<init>(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float){}[0]
final val bottom // coil3.svg/Svg.ViewBox.bottom|{}bottom[0]
final fun <get-bottom>(): kotlin/Float // coil3.svg/Svg.ViewBox.bottom.<get-bottom>|<get-bottom>(){}[0]
final val left // coil3.svg/Svg.ViewBox.left|{}left[0]
final fun <get-left>(): kotlin/Float // coil3.svg/Svg.ViewBox.left.<get-left>|<get-left>(){}[0]
final val right // coil3.svg/Svg.ViewBox.right|{}right[0]
final fun <get-right>(): kotlin/Float // coil3.svg/Svg.ViewBox.right.<get-right>|<get-right>(){}[0]
final val top // coil3.svg/Svg.ViewBox.top|{}top[0]
final fun <get-top>(): kotlin/Float // coil3.svg/Svg.ViewBox.top.<get-top>|<get-top>(){}[0]
final fun component1(): kotlin/Float // coil3.svg/Svg.ViewBox.component1|component1(){}[0]
final fun component2(): kotlin/Float // coil3.svg/Svg.ViewBox.component2|component2(){}[0]
final fun component3(): kotlin/Float // coil3.svg/Svg.ViewBox.component3|component3(){}[0]
final fun component4(): kotlin/Float // coil3.svg/Svg.ViewBox.component4|component4(){}[0]
final fun copy(kotlin/Float = ..., kotlin/Float = ..., kotlin/Float = ..., kotlin/Float = ...): coil3.svg/Svg.ViewBox // coil3.svg/Svg.ViewBox.copy|copy(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.svg/Svg.ViewBox.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // coil3.svg/Svg.ViewBox.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // coil3.svg/Svg.ViewBox.toString|toString(){}[0]
}
}
final class coil3.svg/SvgDecoder : coil3.decode/Decoder { // coil3.svg/SvgDecoder|null[0]
constructor <init>(coil3.decode/ImageSource, coil3.request/Options, coil3.svg/Svg.Parser = ..., kotlin/Function1<coil3/PlatformContext, kotlin/Float> = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // coil3.svg/SvgDecoder.<init>|<init>(coil3.decode.ImageSource;coil3.request.Options;coil3.svg.Svg.Parser;kotlin.Function1<coil3.PlatformContext,kotlin.Float>;kotlin.Boolean;kotlin.Boolean){}[0]
constructor <init>(coil3.decode/ImageSource, coil3.request/Options, kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // coil3.svg/SvgDecoder.<init>|<init>(coil3.decode.ImageSource;coil3.request.Options;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean){}[0]
final val density // coil3.svg/SvgDecoder.density|{}density[0]
final fun <get-density>(): kotlin/Function1<coil3/PlatformContext, kotlin/Float> // coil3.svg/SvgDecoder.density.<get-density>|<get-density>(){}[0]
final val parser // coil3.svg/SvgDecoder.parser|{}parser[0]
final fun <get-parser>(): coil3.svg/Svg.Parser // coil3.svg/SvgDecoder.parser.<get-parser>|<get-parser>(){}[0]
final val renderToBitmap // coil3.svg/SvgDecoder.renderToBitmap|{}renderToBitmap[0]
final fun <get-renderToBitmap>(): kotlin/Boolean // coil3.svg/SvgDecoder.renderToBitmap.<get-renderToBitmap>|<get-renderToBitmap>(){}[0]
final val scaleToDensity // coil3.svg/SvgDecoder.scaleToDensity|{}scaleToDensity[0]
final fun <get-scaleToDensity>(): kotlin/Boolean // coil3.svg/SvgDecoder.scaleToDensity.<get-scaleToDensity>|<get-scaleToDensity>(){}[0]
final val useViewBoundsAsIntrinsicSize // coil3.svg/SvgDecoder.useViewBoundsAsIntrinsicSize|{}useViewBoundsAsIntrinsicSize[0]
final fun <get-useViewBoundsAsIntrinsicSize>(): kotlin/Boolean // coil3.svg/SvgDecoder.useViewBoundsAsIntrinsicSize.<get-useViewBoundsAsIntrinsicSize>|<get-useViewBoundsAsIntrinsicSize>(){}[0]
final suspend fun decode(): coil3.decode/DecodeResult // coil3.svg/SvgDecoder.decode|decode(){}[0]
final class Factory : coil3.decode/Decoder.Factory { // coil3.svg/SvgDecoder.Factory|null[0]
constructor <init>(coil3.svg/Svg.Parser = ..., kotlin/Function1<coil3/PlatformContext, kotlin/Float> = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // coil3.svg/SvgDecoder.Factory.<init>|<init>(coil3.svg.Svg.Parser;kotlin.Function1<coil3.PlatformContext,kotlin.Float>;kotlin.Boolean;kotlin.Boolean){}[0]
constructor <init>(kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // coil3.svg/SvgDecoder.Factory.<init>|<init>(kotlin.Boolean;kotlin.Boolean;kotlin.Boolean){}[0]
final val density // coil3.svg/SvgDecoder.Factory.density|{}density[0]
final fun <get-density>(): kotlin/Function1<coil3/PlatformContext, kotlin/Float> // coil3.svg/SvgDecoder.Factory.density.<get-density>|<get-density>(){}[0]
final val parser // coil3.svg/SvgDecoder.Factory.parser|{}parser[0]
final fun <get-parser>(): coil3.svg/Svg.Parser // coil3.svg/SvgDecoder.Factory.parser.<get-parser>|<get-parser>(){}[0]
final val renderToBitmap // coil3.svg/SvgDecoder.Factory.renderToBitmap|{}renderToBitmap[0]
final fun <get-renderToBitmap>(): kotlin/Boolean // coil3.svg/SvgDecoder.Factory.renderToBitmap.<get-renderToBitmap>|<get-renderToBitmap>(){}[0]
final val scaleToDensity // coil3.svg/SvgDecoder.Factory.scaleToDensity|{}scaleToDensity[0]
final fun <get-scaleToDensity>(): kotlin/Boolean // coil3.svg/SvgDecoder.Factory.scaleToDensity.<get-scaleToDensity>|<get-scaleToDensity>(){}[0]
final val useViewBoundsAsIntrinsicSize // coil3.svg/SvgDecoder.Factory.useViewBoundsAsIntrinsicSize|{}useViewBoundsAsIntrinsicSize[0]
final fun <get-useViewBoundsAsIntrinsicSize>(): kotlin/Boolean // coil3.svg/SvgDecoder.Factory.useViewBoundsAsIntrinsicSize.<get-useViewBoundsAsIntrinsicSize>|<get-useViewBoundsAsIntrinsicSize>(){}[0]
final fun create(coil3.fetch/SourceFetchResult, coil3.request/Options, coil3/ImageLoader): coil3.decode/Decoder? // coil3.svg/SvgDecoder.Factory.create|create(coil3.fetch.SourceFetchResult;coil3.request.Options;coil3.ImageLoader){}[0]
}
final object Companion { // coil3.svg/SvgDecoder.Companion|null[0]
final val NO_DENSITY // coil3.svg/SvgDecoder.Companion.NO_DENSITY|{}NO_DENSITY[0]
final fun <get-NO_DENSITY>(): kotlin/Function1<coil3/PlatformContext, kotlin/Float> // coil3.svg/SvgDecoder.Companion.NO_DENSITY.<get-NO_DENSITY>|<get-NO_DENSITY>(){}[0]
final val PLATFORM_DENSITY // coil3.svg/SvgDecoder.Companion.PLATFORM_DENSITY|{}PLATFORM_DENSITY[0]
final fun <get-PLATFORM_DENSITY>(): kotlin/Function1<coil3/PlatformContext, kotlin/Float> // coil3.svg/SvgDecoder.Companion.PLATFORM_DENSITY.<get-PLATFORM_DENSITY>|<get-PLATFORM_DENSITY>(){}[0]
}
}
final class coil3.svg/SvgImage : coil3/Image { // coil3.svg/SvgImage|null[0]
constructor <init>(org.jetbrains.skia.svg/SVGDOM, kotlin/Int, kotlin/Int) // coil3.svg/SvgImage.<init>|<init>(org.jetbrains.skia.svg.SVGDOM;kotlin.Int;kotlin.Int){}[0]
final val height // coil3.svg/SvgImage.height|{}height[0]
final fun <get-height>(): kotlin/Int // coil3.svg/SvgImage.height.<get-height>|<get-height>(){}[0]
final val shareable // coil3.svg/SvgImage.shareable|{}shareable[0]
final fun <get-shareable>(): kotlin/Boolean // coil3.svg/SvgImage.shareable.<get-shareable>|<get-shareable>(){}[0]
final val size // coil3.svg/SvgImage.size|{}size[0]
final fun <get-size>(): kotlin/Long // coil3.svg/SvgImage.size.<get-size>|<get-size>(){}[0]
final val svg // coil3.svg/SvgImage.svg|{}svg[0]
final fun <get-svg>(): org.jetbrains.skia.svg/SVGDOM // coil3.svg/SvgImage.svg.<get-svg>|<get-svg>(){}[0]
final val width // coil3.svg/SvgImage.width|{}width[0]
final fun <get-width>(): kotlin/Int // coil3.svg/SvgImage.width.<get-width>|<get-width>(){}[0]
final fun draw(org.jetbrains.skia/Canvas) // coil3.svg/SvgImage.draw|draw(org.jetbrains.skia.Canvas){}[0]
}
final val coil3.svg.internal/initHook // coil3.svg.internal/initHook|{}initHook[0]
final fun <get-initHook>(): kotlin/Any // coil3.svg.internal/initHook.<get-initHook>|<get-initHook>(){}[0]
final val coil3.svg/height // coil3.svg/height|@coil3.svg.Svg.ViewBox{}height[0]
final fun (coil3.svg/Svg.ViewBox).<get-height>(): kotlin/Float // coil3.svg/height.<get-height>|<get-height>@coil3.svg.Svg.ViewBox(){}[0]
final val coil3.svg/width // coil3.svg/width|@coil3.svg.Svg.ViewBox{}width[0]
final fun (coil3.svg/Svg.ViewBox).<get-width>(): kotlin/Float // coil3.svg/width.<get-width>|<get-width>@coil3.svg.Svg.ViewBox(){}[0]
final fun (coil3.decode/DecodeUtils).coil3.svg/isSvg(okio/BufferedSource): kotlin/Boolean // coil3.svg/isSvg|isSvg@coil3.decode.DecodeUtils(okio.BufferedSource){}[0]
+55
View File
@@ -0,0 +1,55 @@
// :coil:coil-svg - SVG decoding, vendored VERBATIM from coil-kt/coil into src/vendor.
// Upstream: linux + apple, no mingwX64. The non-android decoder goes through skiko's
// SVGDOM, so mingwX64 binds the bitsycore skiko FORK like :compose:ui:ui-graphics.
// Upstream's non* source sets sit ABOVE nativeMain here (see coil-core).
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
kotlin {
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
applyDefaultHierarchyTemplate()
sourceSets {
commonMain {
dependencies { api(project(":coil:coil-core")) }
kotlin.srcDir("src/vendor/common/kotlin")
}
// Two PARALLEL skiko branches (official for macOS/Linux, the bitsycore fork for
// mingwX64), each two levels deep so nonAndroid `expect`s and nativeMain
// `actual`s are not siblings. See coil/coil-core/build.gradle.kts for the why.
fun skikoBranch(inName: String, inSkiko: Any): org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet {
val vNonAndroid = create("${inName}NonAndroidMain") {
dependsOn(commonMain.get())
kotlin.srcDir("src/vendor/nonAndroid/kotlin")
dependencies { implementation(inSkiko) }
}
return create("${inName}NativeMain") {
dependsOn(vNonAndroid)
kotlin.srcDir("src/vendor/native/kotlin")
}
}
val vOfficial = skikoBranch("coilSvgOfficial", libs.skiko)
get("macosArm64Main").dependsOn(vOfficial)
get("linuxX64Main").dependsOn(vOfficial)
get("linuxArm64Main").dependsOn(vOfficial)
if (vHostSupportsMingw) {
val vFork = skikoBranch(
"coilSvgFork",
"com.bitsycore.skiko:skiko:${providers.gradleProperty("skikoMingwVersion").getOrElse(libs.versions.skikoMingw.get())}",
)
get("mingwX64Main").dependsOn(vFork)
}
}
compilerOptions {
freeCompilerArgs.addAll(
"-Xexpect-actual-classes",
"-opt-in=coil3.annotation.InternalCoilApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=coil3.annotation.DelicateCoilApi",
)
}
}
+44
View File
@@ -0,0 +1,44 @@
# :coil:coil-svg - SVG decoding. Upstream: linux + apple, no mingwX64.
# The non-android decoder goes through skiko's SVGDOM.
# Upstream source sets collapse onto this port: it has NO android, jvm or js
# target, so nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all
# apply to EVERY target here. They land in TWO trees, not one: the non* sets keep
# their own level (src/vendor/nonAndroid) ABOVE src/vendor/native, because some of
# them declare `expect`s whose `actual`s live in upstream nativeMain - collapsing
# both into one source set makes expect and actual siblings, which is an error. appleMain is macosArm64
# only and nonAppleMain is linux + mingw, so those two keep their own trees (the
# build file wires a `nonAppleMain` intermediate; appleMain comes from the default
# hierarchy template).
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
SET_FOLDER=coil-svg/src
commonMain/kotlin/ -> src/vendor/common/kotlin/
# | commonMain/kotlin/coil3/svg/Svg.kt -> src/vendor/common/kotlin/coil3/svg/Svg.kt
# | commonMain/kotlin/coil3/svg/SvgDecoder.kt -> src/vendor/common/kotlin/coil3/svg/SvgDecoder.kt
# | commonMain/kotlin/coil3/svg/decodeUtils.kt -> src/vendor/common/kotlin/coil3/svg/decodeUtils.kt
# | commonMain/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.kt -> src/vendor/common/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.kt
# | commonMain/kotlin/coil3/svg/internal/parseSvg.kt -> src/vendor/common/kotlin/coil3/svg/internal/parseSvg.kt
# | commonMain/kotlin/coil3/svg/internal/utils.kt -> src/vendor/common/kotlin/coil3/svg/internal/utils.kt
nonAndroidMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
# | nonAndroidMain/kotlin/coil3/svg/SvgImage.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/svg/SvgImage.nonAndroid.kt
# | nonAndroidMain/kotlin/coil3/svg/internal/parseSvg.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/svg/internal/parseSvg.nonAndroid.kt
# | nonAndroidMain/kotlin/coil3/svg/internal/utils.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/svg/internal/utils.nonAndroid.kt
nonJvmCommonMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
# | nonJvmCommonMain/kotlin/coil3/svg/internal/utils.nonJvmCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/svg/internal/utils.nonJvmCommon.kt
nativeMain/kotlin/ -> src/vendor/native/kotlin/
# | nativeMain/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.native.kt -> src/vendor/native/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.native.kt
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
# androidMain (4)
# | androidMain/kotlin/coil3/svg/SvgImage.android.kt -> src/vendor/android/kotlin/coil3/svg/SvgImage.android.kt
# | androidMain/kotlin/coil3/svg/imageRequests.android.kt -> src/vendor/android/kotlin/coil3/svg/imageRequests.android.kt
# | androidMain/kotlin/coil3/svg/internal/parseSvg.android.kt -> src/vendor/android/kotlin/coil3/svg/internal/parseSvg.android.kt
# | androidMain/kotlin/coil3/svg/internal/utils.android.kt -> src/vendor/android/kotlin/coil3/svg/internal/utils.android.kt
# jsMain (1)
# | jsMain/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.js.kt -> src/vendor/js/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.js.kt
# jvmCommonMain (1)
# | jvmCommonMain/kotlin/coil3/svg/internal/utils.jvmCommon.kt -> src/vendor/jvmCommon/kotlin/coil3/svg/internal/utils.jvmCommon.kt
# wasmJsMain (1)
# | wasmJsMain/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.wasmJs.kt -> src/vendor/wasmJs/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.wasmJs.kt
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
+19
View File
@@ -0,0 +1,19 @@
// Klib ABI Dump
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <com.bitsycore.coil3:coil>
final object coil3/SingletonImageLoader { // coil3/SingletonImageLoader|null[0]
final fun get(coil3/PlatformContext): coil3/ImageLoader // coil3/SingletonImageLoader.get|get(coil3.PlatformContext){}[0]
final fun reset() // coil3/SingletonImageLoader.reset|reset(){}[0]
final fun setSafe(coil3/SingletonImageLoader.Factory) // coil3/SingletonImageLoader.setSafe|setSafe(coil3.SingletonImageLoader.Factory){}[0]
final fun setUnsafe(coil3/ImageLoader) // coil3/SingletonImageLoader.setUnsafe|setUnsafe(coil3.ImageLoader){}[0]
final fun setUnsafe(coil3/SingletonImageLoader.Factory) // coil3/SingletonImageLoader.setUnsafe|setUnsafe(coil3.SingletonImageLoader.Factory){}[0]
abstract fun interface Factory { // coil3/SingletonImageLoader.Factory|null[0]
abstract fun newImageLoader(coil3/PlatformContext): coil3/ImageLoader // coil3/SingletonImageLoader.Factory.newImageLoader|newImageLoader(coil3.PlatformContext){}[0]
}
}
+34
View File
@@ -0,0 +1,34 @@
// :coil:coil - vendored VERBATIM from coil-kt/coil into src/vendor.
// The singleton ImageLoader facade. Upstream: linux + apple, no mingwX64.
// Upstream's non* source sets sit ABOVE nativeMain here (see coil-core's build file
// and compose-fork.txt for why). Provenance = compose-fork.txt; never hand-edit
// src/vendor - change the manifest + `python scripts/compose-fork/sync.py coil/coil`.
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
kotlin {
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
applyDefaultHierarchyTemplate()
sourceSets {
commonMain {
dependencies { api(project(":coil:coil-core")) }
kotlin.srcDir("src/vendor/common/kotlin")
}
val vNonAndroidMain = create("coilNonAndroidMain") {
dependsOn(commonMain.get())
kotlin.srcDir("src/vendor/nonAndroid/kotlin")
}
nativeMain { dependsOn(vNonAndroidMain) }
}
compilerOptions {
freeCompilerArgs.addAll(
"-Xexpect-actual-classes",
"-opt-in=coil3.annotation.InternalCoilApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=coil3.annotation.DelicateCoilApi",
)
}
}
+24
View File
@@ -0,0 +1,24 @@
# :coil:coil - the singleton ImageLoader facade. Upstream: linux + apple, no mingwX64.
# Upstream source sets collapse onto this port: it has NO android, jvm or js
# target, so nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all
# apply to EVERY target here. They land in TWO trees, not one: the non* sets keep
# their own level (src/vendor/nonAndroid) ABOVE src/vendor/native, because some of
# them declare `expect`s whose `actual`s live in upstream nativeMain - collapsing
# both into one source set makes expect and actual siblings, which is an error. appleMain is macosArm64
# only and nonAppleMain is linux + mingw, so those two keep their own trees (the
# build file wires a `nonAppleMain` intermediate; appleMain comes from the default
# hierarchy template).
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
SET_FOLDER=coil/src
commonMain/kotlin/ -> src/vendor/common/kotlin/
# | commonMain/kotlin/coil3/SingletonImageLoader.kt -> src/vendor/common/kotlin/coil3/SingletonImageLoader.kt
nonAndroidMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
# | nonAndroidMain/kotlin/coil3/SingletonImageLoader.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/SingletonImageLoader.nonAndroid.kt
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
# androidMain (2)
# | androidMain/kotlin/coil3/SingletonImageLoader.android.kt -> src/vendor/android/kotlin/coil3/SingletonImageLoader.android.kt
# | androidMain/kotlin/coil3/singletonImageLoaders.android.kt -> src/vendor/android/kotlin/coil3/singletonImageLoaders.android.kt
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
@@ -1,22 +1,16 @@
// :components-resources — the official Compose Multiplatform resources runtime // :components-resources - the official Compose Multiplatform resources runtime
// (org.jetbrains.compose.components:components-resources), vendored because the // (org.jetbrains.compose.components:components-resources), vendored because the
// Maven artifact ships no mingwX64/linux klibs. Public API is byte-for-byte // Maven artifact ships no mingwX64/linux klibs. Public API is byte-for-byte
// upstream (painterResource / stringResource / Font / qualifiers / Res codegen // upstream (painterResource / stringResource / Font / qualifiers / Res codegen
// compatibility); the platform actuals are this port's: data.kres reading, // compatibility); the platform actuals are this port's: data.kres reading,
// image decode via :ui's Skia decoder, SDL locale/theme environment. Apps' JVM // image decode via :ui's Skia decoder, SDL locale/theme environment. Apps' JVM
// targets keep using the official Maven artifact — this module is native-only. // targets keep using the official Maven artifact - this module is native-only.
plugins { plugins {
alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.plugin.compose) alias(libs.plugins.kotlin.plugin.compose)
} }
repositories {
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
kotlin { kotlin {
@@ -34,13 +28,13 @@ kotlin {
// foundation (isSystemInDarkTheme in ResourceEnvironment) pulls the // foundation (isSystemInDarkTheme in ResourceEnvironment) pulls the
// whole ui surface + loadComposeResourceBytes + the sdl3 cinterop // whole ui surface + loadComposeResourceBytes + the sdl3 cinterop
// types transitively (all edges are api). // types transitively (all edges are api).
api(project(":foundation")) api(project(":compose:foundation:foundation"))
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
} }
} }
nativeMain { nativeMain {
kotlin.srcDir("src/vendor/native/kotlin") kotlin.srcDir("src/vendor/native/kotlin")
// src/nativeMain/kotlin also holds the Font / Image resource actuals — // src/nativeMain/kotlin also holds the Font / Image resource actuals -
// pure project code that delegates to the project font registry // pure project code that delegates to the project font registry
// (IconFont / NamedFont) and the :ui-graphics Skia decoder // (IconFont / NamedFont) and the :ui-graphics Skia decoder
// (decodeEncodedImageBitmap). Skiko-free, so a single native set covers // (decodeEncodedImageBitmap). Skiko-free, so a single native set covers
@@ -1,9 +1,9 @@
# Files vendored VERBATIM from JetBrains/compose-multiplatform (the umbrella repo — # Files vendored VERBATIM from JetBrains/compose-multiplatform (the umbrella repo -
# NOT compose-multiplatform-core) into :components-resources. The official # NOT compose-multiplatform-core) into :components-resources. The official
# org.jetbrains.compose.components:components-resources artifact publishes NO # org.jetbrains.compose.components:components-resources artifact publishes NO
# mingwX64/linux variants, so the runtime library is vendored and the platform # mingwX64/linux variants, so the runtime library is vendored and the platform
# actuals are project code: # actuals are project code:
# src/nativeMain — ResourceReader (data.kres), getSystemEnvironment (SDL), # src/nativeMain - ResourceReader (data.kres), getSystemEnvironment (SDL),
# pure-Kotlin DomXmlParser (upstream's is Darwin NSXMLParser), # pure-Kotlin DomXmlParser (upstream's is Darwin NSXMLParser),
# and the Font/Image resource actuals: renderer-agnostic # and the Font/Image resource actuals: renderer-agnostic
# project code (image decode via the :ui-graphics Skia # project code (image decode via the :ui-graphics Skia
@@ -43,16 +43,16 @@ blockingMain/kotlin/ -> src/vendor/native/kotlin/
nativeMain/kotlin/ -> src/vendor/native/kotlin/ nativeMain/kotlin/ -> src/vendor/native/kotlin/
# | nativeMain/kotlin/org/jetbrains/compose/resources/ImageResources.native.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/ImageResources.native.kt # | nativeMain/kotlin/org/jetbrains/compose/resources/ImageResources.native.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/ImageResources.native.kt
# | nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt # | nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt
# Darwin-only (Foundation NSXMLParser) — replaced by a pure-Kotlin project parser # Darwin-only (Foundation NSXMLParser) - replaced by a pure-Kotlin project parser
# in src/nativeMain so mingwX64/linux compile. # in src/nativeMain so mingwX64/linux compile.
!nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt !nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt
# Portable pieces of upstream skikoMain (no Skia references — pure delegation / # Portable pieces of upstream skikoMain (no Skia references - pure delegation /
# runtime-only) — every native target gets them. # runtime-only) - every native target gets them.
skikoMain/kotlin/org/jetbrains/compose/resources/ImageDecoders.skiko.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/ImageDecoders.skiko.kt skikoMain/kotlin/org/jetbrains/compose/resources/ImageDecoders.skiko.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/ImageDecoders.skiko.kt
skikoMain/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt skikoMain/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt
# Skia-bound pieces — upstream's skikoMain actuals rely on Compose-Desktop Skia # Skia-bound pieces - upstream's skikoMain actuals rely on Compose-Desktop Skia
# extensions (SystemFont / toComposeImageBitmap / nativeCanvas) this port's :ui # extensions (SystemFont / toComposeImageBitmap / nativeCanvas) this port's :ui
# doesn't expose. The module's own Font/Image actuals (src/nativeMain/kotlin/) # doesn't expose. The module's own Font/Image actuals (src/nativeMain/kotlin/)
# use pure project APIs (IconFont / NamedFont / decodeEncodedImageBitmap) # use pure project APIs (IconFont / NamedFont / decodeEncodedImageBitmap)
@@ -1,5 +1,11 @@
@file:OptIn(InternalResourceApi::class, ExperimentalResourceApi::class) @file:OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
// VENDOR-REIMPL(COMPOSE_REF): components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt @ v1.12.0
// Project REIMPLEMENTATION, not a derived copy: same package + signatures,
// different body (the port replaces upstream's skiko scene/platform layer).
// Upstream edits to the counterpart do NOT need reconciling here - re-check
// only if the expect/actual SIGNATURES change.
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import org.jetbrains.compose.resources.InternalResourceApi import org.jetbrains.compose.resources.InternalResourceApi
@@ -15,14 +21,14 @@ import com.compose.sdl.icons.IconFont
import com.compose.sdl.text.NamedFont import com.compose.sdl.text.NamedFont
// ================== // ==================
// MARK: Font actuals — Skia text stack via the project font registry // MARK: Font actuals - Skia text stack via the project font registry
// ================== // ==================
/** The Skia text stack resolves fonts by FAMILY NAME through the project font /** The Skia text stack resolves fonts by FAMILY NAME through the project font
registry (IconFont.registerIcon — the general byte-font table, not just registry (IconFont.registerIcon - the general byte-font table, not just
icons). A font resource therefore loads its bytes through the resource icons). A font resource therefore loads its bytes through the resource
reader, registers them under a per-resource family name, and returns a reader, registers them under a per-resource family name, and returns a
[NamedFont] — `FontFamily(Font(Res.font.x))` then renders through the [NamedFont] - `FontFamily(Font(Res.font.x))` then renders through the
standard project pipeline. Weight/style are carried on the NamedFont; standard project pipeline. Weight/style are carried on the NamedFont;
variation axes apply at draw time via Skia's axis support. */ variation axes apply at draw time via Skia's axis support. */
@@ -1,5 +1,11 @@
@file:OptIn(InternalResourceApi::class, ExperimentalResourceApi::class) @file:OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
// VENDOR-REIMPL(COMPOSE_REF): components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ImageResources.skiko.kt @ v1.12.0
// Project REIMPLEMENTATION, not a derived copy: same package + signatures,
// different body (the port replaces upstream's skiko scene/platform layer).
// Upstream edits to the counterpart do NOT need reconciling here - re-check
// only if the expect/actual SIGNATURES change.
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import org.jetbrains.compose.resources.InternalResourceApi import org.jetbrains.compose.resources.InternalResourceApi
@@ -19,7 +25,7 @@ import com.compose.sdl.graphics.svgIntrinsicSize
import kotlin.math.roundToInt import kotlin.math.roundToInt
// ================== // ==================
// MARK: Image actuals — Skia decode via the :ui-graphics hook // MARK: Image actuals - Skia decode via the :ui-graphics hook
// ================== // ==================
/** Decoding goes through the :ui-graphics Skia hook the SDL backend registers /** Decoding goes through the :ui-graphics Skia hook the SDL backend registers
@@ -28,7 +34,7 @@ import kotlin.math.roundToInt
drawables ship at a single density, so no decode-time rescale applies. */ drawables ship at a single density, so no decode-time rescale applies. */
internal actual fun ByteArray.toImageBitmap(resourceDensity: Int, targetDensity: Int): ImageBitmap = internal actual fun ByteArray.toImageBitmap(resourceDensity: Int, targetDensity: Int): ImageBitmap =
decodeEncodedImageBitmap(this) decodeEncodedImageBitmap(this)
?: error("Image decode failed — is the render backend initialised before painterResource ran?") ?: error("Image decode failed - is the render backend initialised before painterResource ran?")
/** SVG element = the raw document bytes; rendering is size-driven (see SvgPainter). */ /** SVG element = the raw document bytes; rendering is size-driven (see SvgPainter). */
internal actual class SvgElement(val bytes: ByteArray) internal actual class SvgElement(val bytes: ByteArray)
@@ -45,20 +51,20 @@ private class SvgPainter(private val bytes: ByteArray) : Painter() {
override val intrinsicSize: Size = svgIntrinsicSize(bytes) ?: Size.Unspecified override val intrinsicSize: Size = svgIntrinsicSize(bytes) ?: Size.Unspecified
private var fAlpha: Float = 1f private var mAlpha: Float = 1f
private var fColorFilter: ColorFilter? = null private var mColorFilter: ColorFilter? = null
private var fCacheKey: Long = -1L private var mCacheKey: Long = -1L
private var fCacheBitmap: ImageBitmap? = null private var mCacheBitmap: ImageBitmap? = null
override fun applyAlpha(alpha: Float): Boolean { fAlpha = alpha; return true } override fun applyAlpha(alpha: Float): Boolean { mAlpha = alpha; return true }
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { fColorFilter = colorFilter; return true } override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { mColorFilter = colorFilter; return true }
private fun rasterFor(inWidthPx: Int, inHeightPx: Int): ImageBitmap? { private fun rasterFor(inWidthPx: Int, inHeightPx: Int): ImageBitmap? {
val vKey = (inWidthPx.toLong() shl 32) or inHeightPx.toLong() val vKey = (inWidthPx.toLong() shl 32) or inHeightPx.toLong()
if (vKey == fCacheKey && fCacheBitmap != null) return fCacheBitmap if (vKey == mCacheKey && mCacheBitmap != null) return mCacheBitmap
val vBitmap = decodeSvgAt(bytes, inWidthPx, inHeightPx) ?: return null val vBitmap = decodeSvgAt(bytes, inWidthPx, inHeightPx) ?: return null
fCacheKey = vKey mCacheKey = vKey
fCacheBitmap = vBitmap mCacheBitmap = vBitmap
return vBitmap return vBitmap
} }
@@ -73,8 +79,8 @@ private class SvgPainter(private val bytes: ByteArray) : Painter() {
srcSize = IntSize(vBitmap.width, vBitmap.height), srcSize = IntSize(vBitmap.width, vBitmap.height),
dstOffset = IntOffset.Zero, dstOffset = IntOffset.Zero,
dstSize = IntSize(vWpx, vHpx), dstSize = IntSize(vWpx, vHpx),
alpha = fAlpha, alpha = mAlpha,
colorFilter = fColorFilter, colorFilter = mColorFilter,
) )
} }
} }
@@ -8,13 +8,13 @@ import com.compose.sdl.res.preferredLocaleProvider
import com.compose.sdl.res.systemThemeIsDarkProvider import com.compose.sdl.res.systemThemeIsDarkProvider
// ================== // ==================
// MARK: ResourceEnvironment — platform-env-seam actual // MARK: ResourceEnvironment - platform-env-seam actual
// ================== // ==================
/** Non-composable system environment for qualifier resolution (values-fr, /** Non-composable system environment for qualifier resolution (values-fr,
drawable-dark, …): locale + theme come from the platform-env seams installed drawable-dark, …): locale + theme come from the platform-env seams installed
by the SDL layer, so :components-resources carries no dependency on the sdl3 by the SDL layer, so :components-resources carries no dependency on the sdl3
cinterop. Density is reported as 1f — under this port's Option-B density flow cinterop. Density is reported as 1f - under this port's Option-B density flow
layout runs in physical pixels and drawables are bundled at a single density, layout runs in physical pixels and drawables are bundled at a single density,
so the mdpi bucket is always the right one. The COMPOSABLE path so the mdpi bucket is always the right one. The COMPOSABLE path
(rememberResourceEnvironment) doesn't use this: it reads LocalDensity / (rememberResourceEnvironment) doesn't use this: it reads LocalDensity /
@@ -24,7 +24,7 @@ internal actual fun getSystemEnvironment(): ResourceEnvironment {
val vDark = systemThemeIsDarkProvider?.invoke() ?: false val vDark = systemThemeIsDarkProvider?.invoke() ?: false
return ResourceEnvironment( return ResourceEnvironment(
language = LanguageQualifier(vLanguage), language = LanguageQualifier(vLanguage),
// SDL locales carry language+country only — no script subtag. // SDL locales carry language+country only - no script subtag.
script = ScriptQualifier(""), script = ScriptQualifier(""),
region = RegionQualifier(vRegion), region = RegionQualifier(vRegion),
theme = ThemeQualifier.selectByValue(isDark = vDark), theme = ThemeQualifier.selectByValue(isDark = vDark),
@@ -8,11 +8,11 @@ import org.jetbrains.compose.resources.ExperimentalResourceApi
import com.compose.sdl.loadComposeResourceBytes import com.compose.sdl.loadComposeResourceBytes
// ================== // ==================
// MARK: ResourceReader — data.kres actual // MARK: ResourceReader - data.kres actual
// ================== // ==================
/** The platform reader for this port: every app ships its composeResources /** The platform reader for this port: every app ships its composeResources
content inside data.kres (a STORED zip next to the executable — see the content inside data.kres (a STORED zip next to the executable - see the
apps' Zip tasks), and :ui's ResourceIO serves entries by exact path with an apps' Zip tasks), and :ui's ResourceIO serves entries by exact path with an
fseek+fread. The paths the generated Res accessors produce fseek+fread. The paths the generated Res accessors produce
("composeResources/<package>/drawable/x.png") are stored verbatim in ("composeResources/<package>/drawable/x.png") are stored verbatim in
@@ -1,23 +1,23 @@
package org.jetbrains.compose.resources.vector.xmldom package org.jetbrains.compose.resources.vector.xmldom
// ================== // ==================
// MARK: DomXmlParser — pure-Kotlin replacement for upstream's Darwin parser // MARK: DomXmlParser - pure-Kotlin replacement for upstream's Darwin parser
// ================== // ==================
/* Upstream's nativeMain DomXmlParser is built on Foundation's NSXMLParser and /* Upstream's nativeMain DomXmlParser is built on Foundation's NSXMLParser and
only compiles on Darwin; this port also targets mingwX64/linux, so the same only compiles on Darwin; this port also targets mingwX64/linux, so the same
`parse(xml): Element` entry point is implemented as a small recursive-descent `parse(xml): Element` entry point is implemented as a small recursive-descent
XML parser instead (project file — the upstream one is excluded in XML parser instead (project file - the upstream one is excluded in
compose-fork.txt). compose-fork.txt).
Scope: exactly what XmlVectorParser + the resource string XMLs need — Scope: exactly what XmlVectorParser + the resource string XMLs need -
elements, namespaced attributes (xmlns scoping, getAttributeNS, elements, namespaced attributes (xmlns scoping, getAttributeNS,
lookupPrefix), text nodes (textContent), comments / prolog / DOCTYPE / lookupPrefix), text nodes (textContent), comments / prolog / DOCTYPE /
CDATA skipping, and the five predefined entities plus numeric character CDATA skipping, and the five predefined entities plus numeric character
references. No DTD expansion, no processing-instruction handling beyond references. No DTD expansion, no processing-instruction handling beyond
skipping — malformed input throws MalformedXMLException like upstream. */ skipping - malformed input throws MalformedXMLException like upstream. */
// VENDOR-BASE(COMPOSE_REF): components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt @ v1.12.0-beta03+dev4483 // VENDOR-BASE(COMPOSE_REF): components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt @ v1.12.1
// (fresh REIMPL, not a copy-edit — the base ref marks the upstream API it tracks) // (fresh REIMPL, not a copy-edit - the base ref marks the upstream API it tracks)
internal fun parse(xml: String): Element { internal fun parse(xml: String): Element {
val vParser = XmlDomParser(xml) val vParser = XmlDomParser(xml)
@@ -32,13 +32,13 @@ private class Attr(val prefix: String, val localName: String, val nsUri: String,
val qualifiedName: String get() = if (prefix.isEmpty()) localName else "$prefix:$localName" val qualifiedName: String get() = if (prefix.isEmpty()) localName else "$prefix:$localName"
} }
private class NodeListImpl(private val fNodes: List<Node>) : NodeList { private class NodeListImpl(private val mNodes: List<Node>) : NodeList {
override fun item(i: Int): Node = fNodes[i] override fun item(i: Int): Node = mNodes[i]
override val length: Int get() = fNodes.size override val length: Int get() = mNodes.size
} }
private class TextNodeImpl(private val fText: String) : Node { private class TextNodeImpl(private val mText: String) : Node {
override val textContent: String? get() = fText override val textContent: String? get() = mText
override val nodeName: String get() = "#text" override val nodeName: String get() = "#text"
override val localName: String get() = "" override val localName: String get() = ""
override val childNodes: NodeList = NodeListImpl(emptyList()) override val childNodes: NodeList = NodeListImpl(emptyList())
@@ -47,19 +47,19 @@ private class TextNodeImpl(private val fText: String) : Node {
} }
private class ElementImpl( private class ElementImpl(
private val fPrefix: String, private val mPrefix: String,
override val localName: String, override val localName: String,
override val namespaceURI: String, override val namespaceURI: String,
private val fAttrs: List<Attr>, private val mAttrs: List<Attr>,
// Innermost-first chain of xmlns scopes: each is prefix -> uri ("" = default ns). // Innermost-first chain of xmlns scopes: each is prefix -> uri ("" = default ns).
private val fNsScope: List<Map<String, String>>, private val mNsScope: List<Map<String, String>>,
) : Element { ) : Element {
var children: List<Node> = emptyList() var children: List<Node> = emptyList()
override val nodeName: String get() = if (fPrefix.isEmpty()) localName else "$fPrefix:$localName" override val nodeName: String get() = if (mPrefix.isEmpty()) localName else "$mPrefix:$localName"
override val childNodes: NodeList get() = NodeListImpl(children) override val childNodes: NodeList get() = NodeListImpl(children)
// Concatenated descendant text — what DOM's textContent does. // Concatenated descendant text - what DOM's textContent does.
override val textContent: String? override val textContent: String?
get() = buildString { get() = buildString {
for (vChild in children) { for (vChild in children) {
@@ -72,7 +72,7 @@ private class ElementImpl(
override fun lookupPrefix(namespaceURI: String): String { override fun lookupPrefix(namespaceURI: String): String {
if (namespaceURI.isEmpty()) return "" if (namespaceURI.isEmpty()) return ""
for (vScope in fNsScope) { for (vScope in mNsScope) {
for ((vPrefix, vUri) in vScope) { for ((vPrefix, vUri) in vScope) {
if (vUri == namespaceURI && vPrefix.isNotEmpty()) return vPrefix if (vUri == namespaceURI && vPrefix.isNotEmpty()) return vPrefix
} }
@@ -81,22 +81,22 @@ private class ElementImpl(
} }
override fun getAttributeNS(nameSpaceURI: String, localName: String): String = override fun getAttributeNS(nameSpaceURI: String, localName: String): String =
fAttrs.firstOrNull { it.nsUri == nameSpaceURI && it.localName == localName }?.value ?: "" mAttrs.firstOrNull { it.nsUri == nameSpaceURI && it.localName == localName }?.value ?: ""
override fun getAttribute(name: String): String = override fun getAttribute(name: String): String =
fAttrs.firstOrNull { it.qualifiedName == name }?.value ?: "" mAttrs.firstOrNull { it.qualifiedName == name }?.value ?: ""
} }
// ============ // ============
// The parser — one pass over the string with a cursor. // The parser - one pass over the string with a cursor.
private class XmlDomParser(private val fXml: String) { private class XmlDomParser(private val mXml: String) {
private var fPos = 0 private var mPos = 0
fun skipProlog() { fun skipProlog() {
while (true) { while (true) {
skipWhitespace() skipWhitespace()
when { when {
lookingAt("") -> fPos++ lookingAt("") -> mPos++
lookingAt("<?") -> skipUntil("?>") lookingAt("<?") -> skipUntil("?>")
lookingAt("<!--") -> skipUntil("-->") lookingAt("<!--") -> skipUntil("-->")
lookingAt("<!DOCTYPE") -> skipDoctype() lookingAt("<!DOCTYPE") -> skipDoctype()
@@ -112,16 +112,16 @@ private class XmlDomParser(private val fXml: String) {
if (!lookingAt("<") || lookingAt("</")) return null if (!lookingAt("<") || lookingAt("</")) return null
expect("<") expect("<")
val vQName = readName() val vQName = readName()
if (vQName.isEmpty()) throw MalformedXMLException("empty element name at $fPos") if (vQName.isEmpty()) throw MalformedXMLException("empty element name at $mPos")
// Attributes (collect raw first — xmlns declarations shape the scope // Attributes (collect raw first - xmlns declarations shape the scope
// that THIS element's own prefix resolves against). // that THIS element's own prefix resolves against).
val vRawAttrs = ArrayList<Pair<String, String>>() // qualifiedName to value val vRawAttrs = ArrayList<Pair<String, String>>() // qualifiedName to value
while (true) { while (true) {
skipWhitespace() skipWhitespace()
if (lookingAt("/>") || lookingAt(">")) break if (lookingAt("/>") || lookingAt(">")) break
val vAttrName = readName() val vAttrName = readName()
if (vAttrName.isEmpty()) throw MalformedXMLException("bad attribute at $fPos") if (vAttrName.isEmpty()) throw MalformedXMLException("bad attribute at $mPos")
skipWhitespace(); expect("="); skipWhitespace() skipWhitespace(); expect("="); skipWhitespace()
vRawAttrs.add(vAttrName to readQuotedValue()) vRawAttrs.add(vAttrName to readQuotedValue())
} }
@@ -160,7 +160,7 @@ private class XmlDomParser(private val fXml: String) {
val vElement = ElementImpl(vPrefix, vLocal, resolveNs(vPrefix), vAttrs, vScope) val vElement = ElementImpl(vPrefix, vLocal, resolveNs(vPrefix), vAttrs, vScope)
if (lookingAt("/>")) { if (lookingAt("/>")) {
fPos += 2 mPos += 2
return vElement return vElement
} }
expect(">") expect(">")
@@ -169,9 +169,9 @@ private class XmlDomParser(private val fXml: String) {
val vChildren = ArrayList<Node>() val vChildren = ArrayList<Node>()
while (true) { while (true) {
when { when {
fPos >= fXml.length -> throw MalformedXMLException("unclosed <$vQName>") mPos >= mXml.length -> throw MalformedXMLException("unclosed <$vQName>")
lookingAt("</") -> { lookingAt("</") -> {
fPos += 2 mPos += 2
val vClose = readName() val vClose = readName()
skipWhitespace(); expect(">") skipWhitespace(); expect(">")
if (vClose != vQName) throw MalformedXMLException("mismatched </$vClose> for <$vQName>") if (vClose != vQName) throw MalformedXMLException("mismatched </$vClose> for <$vQName>")
@@ -180,18 +180,18 @@ private class XmlDomParser(private val fXml: String) {
} }
lookingAt("<!--") -> skipUntil("-->") lookingAt("<!--") -> skipUntil("-->")
lookingAt("<![CDATA[") -> { lookingAt("<![CDATA[") -> {
val vEnd = fXml.indexOf("]]>", fPos + 9) val vEnd = mXml.indexOf("]]>", mPos + 9)
if (vEnd < 0) throw MalformedXMLException("unclosed CDATA") if (vEnd < 0) throw MalformedXMLException("unclosed CDATA")
vChildren.add(TextNodeImpl(fXml.substring(fPos + 9, vEnd))) vChildren.add(TextNodeImpl(mXml.substring(mPos + 9, vEnd)))
fPos = vEnd + 3 mPos = vEnd + 3
} }
lookingAt("<?") -> skipUntil("?>") lookingAt("<?") -> skipUntil("?>")
lookingAt("<") -> parseElement(vScope)?.let { vChildren.add(it) } lookingAt("<") -> parseElement(vScope)?.let { vChildren.add(it) }
else -> { else -> {
val vNext = fXml.indexOf('<', fPos).let { if (it < 0) fXml.length else it } val vNext = mXml.indexOf('<', mPos).let { if (it < 0) mXml.length else it }
val vText = decodeEntities(fXml.substring(fPos, vNext)) val vText = decodeEntities(mXml.substring(mPos, vNext))
if (vText.isNotBlank()) vChildren.add(TextNodeImpl(vText)) if (vText.isNotBlank()) vChildren.add(TextNodeImpl(vText))
fPos = vNext mPos = vNext
} }
} }
} }
@@ -200,28 +200,28 @@ private class XmlDomParser(private val fXml: String) {
// ============ // ============
// Lexing helpers // Lexing helpers
private fun lookingAt(inToken: String): Boolean = fXml.startsWith(inToken, fPos) private fun lookingAt(inToken: String): Boolean = mXml.startsWith(inToken, mPos)
private fun expect(inToken: String) { private fun expect(inToken: String) {
if (!lookingAt(inToken)) throw MalformedXMLException("expected '$inToken' at $fPos") if (!lookingAt(inToken)) throw MalformedXMLException("expected '$inToken' at $mPos")
fPos += inToken.length mPos += inToken.length
} }
private fun skipWhitespace() { private fun skipWhitespace() {
while (fPos < fXml.length && fXml[fPos].isWhitespace()) fPos++ while (mPos < mXml.length && mXml[mPos].isWhitespace()) mPos++
} }
private fun skipUntil(inToken: String) { private fun skipUntil(inToken: String) {
val vEnd = fXml.indexOf(inToken, fPos) val vEnd = mXml.indexOf(inToken, mPos)
if (vEnd < 0) throw MalformedXMLException("unterminated '$inToken' section") if (vEnd < 0) throw MalformedXMLException("unterminated '$inToken' section")
fPos = vEnd + inToken.length mPos = vEnd + inToken.length
} }
/* DOCTYPE may nest an internal subset in [ ] — skip to the matching '>'. */ /* DOCTYPE may nest an internal subset in [ ] - skip to the matching '>'. */
private fun skipDoctype() { private fun skipDoctype() {
var vDepth = 0 var vDepth = 0
while (fPos < fXml.length) { while (mPos < mXml.length) {
val c = fXml[fPos++] val c = mXml[mPos++]
if (c == '[') vDepth++ if (c == '[') vDepth++
else if (c == ']') vDepth-- else if (c == ']') vDepth--
else if (c == '>' && vDepth <= 0) return else if (c == '>' && vDepth <= 0) return
@@ -230,24 +230,24 @@ private class XmlDomParser(private val fXml: String) {
} }
private fun readName(): String { private fun readName(): String {
val vStart = fPos val vStart = mPos
while (fPos < fXml.length) { while (mPos < mXml.length) {
val c = fXml[fPos] val c = mXml[mPos]
if (c.isWhitespace() || c == '=' || c == '>' || c == '/' || c == '<') break if (c.isWhitespace() || c == '=' || c == '>' || c == '/' || c == '<') break
fPos++ mPos++
} }
return fXml.substring(vStart, fPos) return mXml.substring(vStart, mPos)
} }
private fun readQuotedValue(): String { private fun readQuotedValue(): String {
if (fPos >= fXml.length) throw MalformedXMLException("unterminated attribute value") if (mPos >= mXml.length) throw MalformedXMLException("unterminated attribute value")
val vQuote = fXml[fPos] val vQuote = mXml[mPos]
if (vQuote != '"' && vQuote != '\'') throw MalformedXMLException("attribute value must be quoted at $fPos") if (vQuote != '"' && vQuote != '\'') throw MalformedXMLException("attribute value must be quoted at $mPos")
fPos++ mPos++
val vEnd = fXml.indexOf(vQuote, fPos) val vEnd = mXml.indexOf(vQuote, mPos)
if (vEnd < 0) throw MalformedXMLException("unterminated attribute value") if (vEnd < 0) throw MalformedXMLException("unterminated attribute value")
val vRaw = fXml.substring(fPos, vEnd) val vRaw = mXml.substring(mPos, vEnd)
fPos = vEnd + 1 mPos = vEnd + 1
return decodeEntities(vRaw) return decodeEntities(vRaw)
} }
@@ -1,11 +1,11 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
// :animation-core — androidx.compose.animation.core.* vendored VERBATIM from // :animation-core - androidx.compose.animation.core.* vendored VERBATIM from
// upstream. Renderer-agnostic (spring/tween specs, animatable, transition, // upstream. Renderer-agnostic (spring/tween specs, animatable, transition,
// vector math). Split out of :ui so :ui stays about renderer+ui glue only. // vector math). Split out of :ui so :ui stays about renderer+ui glue only.
// //
// Provenance = animation-core/compose-fork.txt + scripts/compose-fork/compose.properties. // Provenance = animation-core/compose-fork.txt + scripts/compose-fork/compose.properties.
// Never hand-edit files under animation-core/src/vendor/ — change the manifest and // Never hand-edit files under animation-core/src/vendor/ - change the manifest and
// re-run `bash scripts/compose-fork/sync.sh :animation-core`. // re-run `bash scripts/compose-fork/sync.sh :animation-core`.
// //
// Publication artifactId (when set up): compose-desktop-native-animation-core. // Publication artifactId (when set up): compose-desktop-native-animation-core.
@@ -30,7 +30,7 @@ kotlin {
commonMain { commonMain {
kotlin.srcDir("src/vendor/common/kotlin") kotlin.srcDir("src/vendor/common/kotlin")
dependencies { dependencies {
api(project(":ui")) api(project(":compose:ui:ui"))
} }
} }
nativeMain { nativeMain {
@@ -1,9 +1,9 @@
// :animation-graphics — androidx.compose.animation.graphics.*, vendored VERBATIM // :animation-graphics - androidx.compose.animation.graphics.*, vendored VERBATIM
// from upstream into src/vendor/. Split out to mirror Compose Multiplatform's // from upstream into src/vendor/. Split out to mirror Compose Multiplatform's
// module packaging (compose/animation/animation-graphics). // module packaging (compose/animation/animation-graphics).
// //
// Provenance = animation-graphics/compose-fork.txt + scripts/compose-fork/compose.properties. // Provenance = animation-graphics/compose-fork.txt + scripts/compose-fork/compose.properties.
// Never hand-edit files under src/vendor/ — change the manifest and re-run // Never hand-edit files under src/vendor/ - change the manifest and re-run
// `python scripts/compose-fork/sync.py compose/animation/animation-graphics`. // `python scripts/compose-fork/sync.py compose/animation/animation-graphics`.
// //
// Publication artifactId: desktop-animation-graphics. // Publication artifactId: desktop-animation-graphics.
@@ -28,8 +28,8 @@ kotlin {
dependencies { dependencies {
// animation.graphics.* uses animation.* / animation.core.* and the vector // animation.graphics.* uses animation.* / animation.core.* and the vector
// graphics from ui (ui-graphics.vector, ui-util). // graphics from ui (ui-graphics.vector, ui-util).
api(project(":animation")) api(project(":compose:animation:animation"))
api(project(":ui")) api(project(":compose:ui:ui"))
} }
kotlin.srcDir("src/vendor/common/kotlin") kotlin.srcDir("src/vendor/common/kotlin")
} }
+4 -4
View File
@@ -1,9 +1,9 @@
// :animation — androidx.compose.animation.* (non-core), vendored VERBATIM from // :animation - androidx.compose.animation.* (non-core), vendored VERBATIM from
// upstream into src/vendor/. Split out of :foundation to mirror Compose // upstream into src/vendor/. Split out of :foundation to mirror Compose
// Multiplatform's module packaging. // Multiplatform's module packaging.
// //
// Provenance = animation/compose-fork.txt + scripts/compose-fork/compose.properties. // Provenance = animation/compose-fork.txt + scripts/compose-fork/compose.properties.
// Never hand-edit files under src/vendor/ — change the manifest and re-run // Never hand-edit files under src/vendor/ - change the manifest and re-run
// `python scripts/compose-fork/sync.py compose/animation/animation`. // `python scripts/compose-fork/sync.py compose/animation/animation`.
// //
// Publication artifactId: desktop-animation. // Publication artifactId: desktop-animation.
@@ -28,8 +28,8 @@ kotlin {
dependencies { dependencies {
// animation.* imports animation.core.* and foundation.layout.* (Box/Row/Column); // animation.* imports animation.core.* and foundation.layout.* (Box/Row/Column);
// ui / runtime arrive transitively. // ui / runtime arrive transitively.
api(project(":animation-core")) api(project(":compose:animation:animation-core"))
api(project(":foundation-layout")) api(project(":compose:foundation:foundation-layout"))
} }
kotlin.srcDir("src/vendor/common/kotlin") kotlin.srcDir("src/vendor/common/kotlin")
} }
@@ -28,8 +28,8 @@ final var com.compose.sdl/useVirtualFrameTime // com.compose.sdl/useVirtualFrame
final fun <get-useVirtualFrameTime>(): kotlin/Boolean // com.compose.sdl/useVirtualFrameTime.<get-useVirtualFrameTime>|<get-useVirtualFrameTime>(){}[0] final fun <get-useVirtualFrameTime>(): kotlin/Boolean // com.compose.sdl/useVirtualFrameTime.<get-useVirtualFrameTime>|<get-useVirtualFrameTime>(){}[0]
final fun <set-useVirtualFrameTime>(kotlin/Boolean) // com.compose.sdl/useVirtualFrameTime.<set-useVirtualFrameTime>|<set-useVirtualFrameTime>(kotlin.Boolean){}[0] final fun <set-useVirtualFrameTime>(kotlin/Boolean) // com.compose.sdl/useVirtualFrameTime.<set-useVirtualFrameTime>|<set-useVirtualFrameTime>(kotlin.Boolean){}[0]
final fun (com.compose.sdl/ApplicationScope).com.compose.sdl/Window(kotlin/Function0<kotlin/Unit>, kotlin/String?, kotlin/Int, kotlin/Int, com.compose.sdl/GpuMode?, com.compose.sdl/AppWindowIcon?, kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>?, kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl/Window|Window@com.compose.sdl.ApplicationScope(kotlin.Function0<kotlin.Unit>;kotlin.String?;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode?;com.compose.sdl.AppWindowIcon?;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun (com.compose.sdl/ApplicationScope).com.compose.sdl/Window(kotlin/Function0<kotlin/Unit>, kotlin/String?, kotlin/Int, kotlin/Int, com.compose.sdl/GpuMode?, com.compose.sdl/AppWindowIcon?, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean>?, kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean>?, kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>?, kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // com.compose.sdl/Window|Window@com.compose.sdl.ApplicationScope(kotlin.Function0<kotlin.Unit>;kotlin.String?;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode?;com.compose.sdl.AppWindowIcon?;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>?;kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>?;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter|com_compose_sdl_AppWindowIcon$stableprop_getter(){}[0] final fun com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter|com_compose_sdl_AppWindowIcon$stableprop_getter(){}[0]
final fun com.compose.sdl/nativeComposeApp(kotlin/Function3<com.compose.sdl/ApplicationScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeApp|nativeComposeApp(kotlin.Function3<com.compose.sdl.ApplicationScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0] final fun com.compose.sdl/nativeComposeApp(kotlin/Function3<com.compose.sdl/ApplicationScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeApp|nativeComposeApp(kotlin.Function3<com.compose.sdl.ApplicationScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
final fun com.compose.sdl/nativeComposeWindow(kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., com.compose.sdl/GpuMode = ..., com.compose.sdl/AppWindowIcon? = ..., kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>? = ..., kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeWindow|nativeComposeWindow(kotlin.String;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode;com.compose.sdl.AppWindowIcon?;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0] final fun com.compose.sdl/nativeComposeWindow(kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., com.compose.sdl/GpuMode = ..., com.compose.sdl/AppWindowIcon? = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean> = ..., kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean> = ..., kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>? = ..., kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeWindow|nativeComposeWindow(kotlin.String;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode;com.compose.sdl.AppWindowIcon?;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>;kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
final fun com.compose.sdl/windowHasInvalidations(): kotlin/Boolean // com.compose.sdl/windowHasInvalidations|windowHasInvalidations(){}[0] final fun com.compose.sdl/windowHasInvalidations(): kotlin/Boolean // com.compose.sdl/windowHasInvalidations|windowHasInvalidations(){}[0]
@@ -1,7 +1,7 @@
// :desktop-native-window — the module apps depend on. Owns nativeComposeWindow() (main loop, // :desktop-native-window - the module apps depend on. Owns nativeComposeWindow() (main loop,
// recomposer lifecycle, event dispatch, Snapshot apply notifications). // recomposer lifecycle, event dispatch, Snapshot apply notifications).
// Renderer selection happens entirely inside :ui via source-set wiring // Renderer selection happens entirely inside :ui via source-set wiring
// (skikoRendererMain — Skia-only) — this module just calls // (skikoRendererMain - Skia-only) - this module just calls
// `createRenderBackend(...)` and `rendererPreferredGpuMode()` from :ui and // `createRenderBackend(...)` and `rendererPreferredGpuMode()` from :ui and
// the right symbol resolves per target. // the right symbol resolves per target.
// Publication artifactId (when set up): compose-desktop-native. // Publication artifactId (when set up): compose-desktop-native.
@@ -23,7 +23,7 @@ kotlin {
applyDefaultHierarchyTemplate() applyDefaultHierarchyTemplate()
// sdl3.* types are api-exposed via :ui → :sdl-core's cinterop klib — no // sdl3.* types are api-exposed via :ui → :sdl-core's cinterop klib - no
// separate sdl3 cinterop here. // separate sdl3 cinterop here.
sourceSets { sourceSets {
@@ -34,14 +34,14 @@ kotlin {
// out of :ui (upstream Compose layout). // out of :ui (upstream Compose layout).
// Material widgets used to be re-exported from :material here; the // Material widgets used to be re-exported from :material here; the
// module was retired when :apidemo and :demo migrated to :material3. // module was retired when :apidemo and :demo migrated to :material3.
// Apps that want Material 3 widgets pull `implementation(project(":material3"))` // Apps that want Material 3 widgets pull `implementation(project(":compose:material3:material3"))`
// themselves (:material3 doesn't need to be `api`-exposed — the // themselves (:material3 doesn't need to be `api`-exposed - the
// upstream vendored surface is stable and apps import it directly). // upstream vendored surface is stable and apps import it directly).
api(project(":ui")) api(project(":compose:ui:ui"))
api(project(":foundation")) api(project(":compose:foundation:foundation"))
api(project(":animation-core")) api(project(":compose:animation:animation-core"))
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
// setMain() / resetMain() — see Sdl3MainDispatcher.kt for usage. // setMain() / resetMain() - see Sdl3MainDispatcher.kt for usage.
implementation(libs.kotlinx.coroutines.test) implementation(libs.kotlinx.coroutines.test)
} }
} }
@@ -0,0 +1,88 @@
@file:OptIn(
androidx.compose.ui.InternalComposeUiApi::class,
androidx.compose.runtime.InternalComposeApi::class,
)
package com.compose.sdl
import androidx.compose.runtime.AbstractApplier
import androidx.compose.runtime.Composable
import kotlinx.coroutines.CoroutineScope
// ==================
// MARK: App runtime internals
// ==================
internal class ApplicationScopeImpl(val runtime: AppRuntime) : ApplicationScope {
override fun exitApplication() { runtime.exitRequested = true }
}
/** Registry + lifecycle for the live windows. Windows are created by the app
composition (Window()'s remember) and destroyed by the LOOP - DisposableEffect
onDispose only schedules, because teardown disposes a composition and that
must not run re-entrantly inside another composition's apply pass. */
internal class AppRuntime {
lateinit var scope: CoroutineScope
val windows = mutableListOf<WindowInstance>()
private val destroyQueue = mutableListOf<WindowInstance>()
var exitRequested = false
var hadWindow = false
fun windowFor(inId: UInt): WindowInstance? =
if (inId == 0u) windows.firstOrNull()
else windows.firstOrNull { it.backend.windowId == inId } ?: windows.firstOrNull()
fun markAllNeedFrame() {
for (vW in windows) vW.needsFrame = true
}
fun createWindow(
inTitle: String,
inWidth: Int,
inHeight: Int,
inGpu: GpuMode,
inIcon: AppWindowIcon?,
inAttrs: WindowAttributes,
inOnFrame: ((RenderBackend, Int) -> Boolean)?,
inPreviewKeyHandler: () -> ((androidx.compose.ui.input.key.KeyEvent) -> Boolean),
inKeyHandler: () -> ((androidx.compose.ui.input.key.KeyEvent) -> Boolean),
inContent: () -> (@Composable ComposeWindowScope.() -> Unit),
): WindowInstance {
val vWindow = WindowInstance(
inTitle, inWidth, inHeight, inGpu, inIcon, inAttrs, inOnFrame,
inPreviewKeyHandler, inKeyHandler, inContent,
)
// A Window() was declared either way - the "exit when the last window
// is gone" rule must also fire when every declared window failed to
// initialise (otherwise the loop would spin forever with none).
hadWindow = true
if (vWindow.init(scope)) {
windows.add(vWindow)
} else {
println("nativeComposeApp: window '$inTitle' failed to initialise")
}
return vWindow
}
fun scheduleDestroy(inWindow: WindowInstance) {
if (inWindow in windows && inWindow !in destroyQueue) destroyQueue.add(inWindow)
}
fun reapDestroyed() {
while (destroyQueue.isNotEmpty()) {
val vW = destroyQueue.removeLast()
windows.remove(vW)
vW.destroy()
}
}
}
// No-op applier for the app-level composition - it emits no UI nodes, only
// side effects (each Window() manages a native window).
internal class UnitApplier : AbstractApplier<Unit>(Unit) {
override fun insertTopDown(index: Int, instance: Unit) {}
override fun insertBottomUp(index: Int, instance: Unit) {}
override fun remove(index: Int, count: Int) {}
override fun move(from: Int, to: Int, count: Int) {}
override fun onClear() {}
}
@@ -11,14 +11,14 @@ package com.compose.sdl
* *
* Each list holds data.kres resource paths of PRE-DECODED `.rgba` icon blobs * Each list holds data.kres resource paths of PRE-DECODED `.rgba` icon blobs
* (produced by the bridge plugin's `icon { }` packaging, or by * (produced by the bridge plugin's `icon { }` packaging, or by
* `scripts/make-app-icon.py rgba` — i.e. an 8-byte * `scripts/make-app-icon.py rgba` - i.e. an 8-byte
* `[width u32-le][height u32-le]` header + straight-alpha RGBA pixels). List * `[width u32-le][height u32-le]` header + straight-alpha RGBA pixels). List
* one path per size you bundle; the largest becomes the base and the rest are * one path per size you bundle; the largest becomes the base and the rest are
* attached as alternate resolutions SDL chooses from (title bar vs taskbar vs * attached as alternate resolutions SDL chooses from (title bar vs taskbar vs
* Alt-Tab), so the icon stays crisp at every size. * Alt-Tab), so the icon stays crisp at every size.
* *
* On Windows the runtime icon set here complements the icon EMBEDDED in the * On Windows the runtime icon set here complements the icon EMBEDDED in the
* `.exe` (Explorer / pinned taskbar) — see the app / bridge-plugin Gradle setup. * `.exe` (Explorer / pinned taskbar) - see the app / bridge-plugin Gradle setup.
*/ */
class AppWindowIcon( class AppWindowIcon(
val light: List<String>, val light: List<String>,
@@ -0,0 +1,357 @@
@file:OptIn(
androidx.compose.ui.InternalComposeUiApi::class,
androidx.compose.runtime.InternalComposeApi::class,
)
package com.compose.sdl
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.runtime.Composition
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshots.Snapshot
import kotlinx.coroutines.*
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import sdl3.SDL_Delay
import sdl3.SDL_GetTicks
import sdl3.SDL_Quit
import sdl3.SDL_WaitEventTimeout
// ==================
// MARK: Application entry - nativeComposeApp + Window()
// ==================
/**
Multi-window entry point, shaped like Compose Desktop's `application {}`:
nativeComposeApp {
var showTools by remember { mutableStateOf(false) }
Window(onCloseRequest = ::exitApplication, title = "Main") { MainUi(onTools = { showTools = true }) }
if (showTools) Window(onCloseRequest = { showTools = false }, title = "Tools") { ToolsUi() }
}
The app CONTENT is itself a composition (no UI tree - a Unit applier): each
`Window(...)` call materialises an SDL window + renderer + root host + its own
recomposer/composition, and leaving the composition (state flips to false)
tears the window down. One main loop pumps the shared SDL event queue and
routes events per SDL window id.
Per-window pumping order matters: the render-bridge globals
(currentImageLoader / viewport) are per-renderer, so the
loop installs a window's globals before recomposing / laying out / drawing it.
`nativeComposeWindow(...)` remains as the single-window wrapper (all demo
probes and apidemo ride it unchanged).
*/
interface ApplicationScope {
/** Requests the main loop to exit after the current iteration (all windows
are torn down on the way out). */
fun exitApplication()
}
/** Declares one native window for as long as this composable stays in the app
composition. `onCloseRequest` fires when the user asks the window to close
(OS close button, or `window.close()` from content) - remove the state that
composes this Window to actually close it, or call exitApplication(). */
@Composable
fun ApplicationScope.Window(
onCloseRequest: () -> Unit,
title: String = "ComposeNativeSDL3",
width: Int = 800,
height: Int = 600,
gpu: GpuMode = GpuMode.Auto,
icon: AppWindowIcon? = null,
undecorated: Boolean = false,
transparent: Boolean = false,
resizable: Boolean = true,
enabled: Boolean = true,
focusable: Boolean = true,
alwaysOnTop: Boolean = false,
onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
onKeyEvent: (KeyEvent) -> Boolean = { false },
onFrame: ((backend: RenderBackend, frameIndex: Int) -> Boolean)? = null,
content: @Composable ComposeWindowScope.() -> Unit,
) {
val vScope = this as? ApplicationScopeImpl
?: error("Window() must be called inside nativeComposeApp { ... }")
val vContent = rememberUpdatedState(content)
val vClose = rememberUpdatedState(onCloseRequest)
val vPreviewKey = rememberUpdatedState(onPreviewKeyEvent)
val vKey = rememberUpdatedState(onKeyEvent)
val vAttrs = WindowAttributes(
undecorated = undecorated,
transparent = transparent,
resizable = resizable,
enabled = enabled,
focusable = focusable,
alwaysOnTop = alwaysOnTop,
)
val vWindow = remember {
vScope.runtime.createWindow(
inTitle = title,
inWidth = width,
inHeight = height,
inGpu = gpu,
inIcon = icon,
inAttrs = vAttrs,
inOnFrame = onFrame,
// Read through holders so updated lambdas propagate without
// recreating the window (same reason as the content holder below).
inPreviewKeyHandler = { vPreviewKey.value },
inKeyHandler = { vKey.value },
// The window composition reads the holder each recomposition, so
// updated content lambdas propagate without recreating the window.
inContent = { vContent.value },
).also { it.onCloseRequest = { vClose.value.invoke() } }
}
SideEffect {
if (vWindow.facade.title != title) vWindow.facade.setTitle(title)
}
// Re-apply only on a real change; `transparent` is creation-only (see
// WindowAttributes) so it is deliberately not re-applied here.
DisposableEffect(vAttrs) {
vWindow.applyAttributes(vAttrs)
onDispose { }
}
DisposableEffect(Unit) {
onDispose { vScope.runtime.scheduleDestroy(vWindow) }
}
}
/** Boots SDL + the Compose runtime, runs `content` as the application
composition, and drives every declared Window until exitApplication() or
the last window closes. */
fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
// Install the Main dispatcher BEFORE any window/owner exists: ComposeOwner
// captures Dispatchers.Main eagerly for its per-node coroutine scopes.
val mainDispatcher = Sdl3MainDispatcher()
@OptIn(ExperimentalCoroutinesApi::class)
Dispatchers.setMain(mainDispatcher)
val runtime = AppRuntime()
runBlocking {
runtime.scope = this
// App-level composition: no UI tree - it only declares Window()s.
val appClock = Sdl3FrameClock()
val appRecomposer = Recomposer(coroutineContext + appClock)
// The clock must be in the collector's context - runRecomposeAndApplyChanges
// awaits parent frames through it.
val appRecomposeJob = launch(appClock) { appRecomposer.runRecomposeAndApplyChanges() }
val appComposition = Composition(UnitApplier(), appRecomposer)
val appScope = ApplicationScopeImpl(runtime)
val snapshotHandle = Snapshot.registerGlobalWriteObserver {
// Only SCHEDULE a frame here; the apply is coalesced to once per loop
// iteration (top of loop + before each pump + before layout in
// renderFrame) instead of walking every snapshot observer on every
// individual state write. Mirrors upstream GlobalSnapshotManager,
// which schedules rather than applying inline.
runtime.markAllNeedFrame()
}
appComposition.setContent { appScope.content() }
// Compose the initial Window() declarations before entering the loop.
appClock.sendFrame(frameClockNanos())
yield()
// ============
// Main loop
var vGcLastTicks = SDL_GetTicks()
var vRenderedSinceGc = false
while (!runtime.exitRequested) {
FrameProfiler.mark()
// One virtual 16.6ms tick per loop iteration (no-op unless useVirtualFrameTime) -
// all windows' clocks share the same timestamp this iteration.
advanceVirtualFrame()
Snapshot.sendApplyNotifications()
// ============
// Events - one shared SDL queue, routed by window id.
val vEvents = pollEvents()
for (vEvent in vEvents) {
when (vEvent) {
is AppEvent.Quit -> {
// Platform quit = close request on every window; any
// veto (setOnCloseRequest returning false) keeps the
// app alive. No windows at all → plain exit.
if (runtime.windows.isEmpty()) runtime.exitRequested = true
else for (vW in runtime.windows.toList()) vW.requestClose()
}
is AppEvent.WindowClose -> runtime.windowFor(vEvent.windowId)?.requestClose()
is AppEvent.WindowResized -> runtime.windowFor(vEvent.windowId)?.onResizedEvent()
is AppEvent.RedrawNeeded -> runtime.windowFor(vEvent.windowId)?.let { it.needsFrame = true }
is AppEvent.SystemThemeChanged -> for (vW in runtime.windows) vW.onSystemThemeChanged()
is AppEvent.WindowActivation -> runtime.windowFor(vEvent.windowId)?.onActivationEvent(vEvent)
is AppEvent.Pointer -> runtime.windowFor(vEvent.windowId)?.onPointerEvent(vEvent)
is AppEvent.PointerExit -> runtime.windowFor(vEvent.windowId)?.onPointerExit()
is AppEvent.MouseWheel -> runtime.windowFor(vEvent.windowId)?.onWheelEvent(vEvent)
is AppEvent.Key -> runtime.windowFor(vEvent.windowId)?.onKeyEvent(vEvent)
is AppEvent.TextInput -> runtime.windowFor(vEvent.windowId)?.onTextInputEvent(vEvent)
is AppEvent.TextEditing -> runtime.windowFor(vEvent.windowId)?.onTextEditingEvent(vEvent)
is AppEvent.Drop -> runtime.windowFor(vEvent.windowId)?.onDropEvent(vEvent)
}
}
mainDispatcher.drainPending()
FrameProfiler.phase("events")
// ============
// App composition pump - Window()s may appear / disappear here.
Snapshot.sendApplyNotifications()
appClock.sendFrame(frameClockNanos())
yield()
runtime.reapDestroyed()
// Window content can flip window.close() / probes can end the app.
for (vW in runtime.windows.toList()) {
if (vW.facade.isCloseRequested && !vW.closeDispatched) {
vW.closeDispatched = true
vW.onCloseRequest()
}
}
// Exit when the last window is gone (after at least one existed).
if (runtime.hadWindow && runtime.windows.isEmpty()) runtime.exitRequested = true
if (runtime.exitRequested) break
FrameProfiler.phase("app")
// ============
// Per-window pump + render.
var vAnyRendered = false
var vAllVsync = true
// Non-vsync fallback cap: the shortest frame interval among the
// rendered non-vsync windows' displays (60Hz default). Only used when
// no window is vsync-paced (Software renderer / vsync unavailable).
var vFallbackDelayMs = 16u
val vAppPending = appRecomposer.hasPendingWork
for (vW in runtime.windows.toList()) {
vW.installGlobals()
vW.host.sendAnimationFrame(animationClockNanos())
mainDispatcher.drainPending()
Snapshot.sendApplyNotifications()
vW.frameClock.sendFrame(frameClockNanos())
yield()
FrameProfiler.phase("pump")
if (vW.shouldRender()) {
vW.renderFrame()
vAnyRendered = true
if (!vW.backend.vsyncEnabled) {
vAllVsync = false
vFallbackDelayMs = minOf(vFallbackDelayMs, displayFrameDelayMs(vW.backend.window))
}
}
FrameProfiler.phase("render")
}
runtime.reapDestroyed()
FrameProfiler.frameDone(vAnyRendered)
// ============
// Drain deferred native-resource disposals on the MAIN thread -
// textures/surfaces whose owner closed them or whose Cleaner fired
// on a GC worker enqueue here (SDL calls aren't thread-safe). This
// is the ownership path that makes the GC nudge below a mere
// backstop (ROADMAP.md item 1).
com.compose.sdl.graphics.NativeReleaseQueue.drain()
// ============
// Pace / idle-skip.
if (vAnyRendered) {
SDL_Delay(if (vAllVsync) 1u else vFallbackDelayMs)
} else if (!vAppPending) {
// Nothing invalidated anywhere: block for events instead of
// spinning. A real input/redraw event wakes this immediately; the
// timeout only bounds how often async work (dispatcher/timers) is
// re-checked while truly idle, so keep it coarse to cut wakeups.
SDL_WaitEventTimeout(null, 100)
for (vW in runtime.windows) vW.resetFpsWindow()
} else {
// App composition has pending work but nothing rendered this
// iteration - yield briefly instead of spinning at full speed
// until the composition settles into a renderable state.
SDL_Delay(1u)
}
// ============
// Native-memory nudge. Renderer resources (Skia surfaces / images /
// fonts, SDL textures) are freed by Cleaners that only run when the
// Kotlin/Native GC collects - and a Compose app's Kotlin heap is
// small enough that the allocation-driven scheduler can starve them
// for minutes while the NATIVE heap balloons (issue #2: memory
// "never released" while navigating). Collect periodically, only
// after rendering activity, between frames; costs ~ms at these
// heap sizes and keeps RSS tracking real usage.
if (vAnyRendered) vRenderedSinceGc = true
val vNowTicks = SDL_GetTicks()
if (vRenderedSinceGc && vNowTicks - vGcLastTicks >= 10_000uL) {
vGcLastTicks = vNowTicks
vRenderedSinceGc = false
collectNativeGarbage()
}
}
// ============
// Teardown
snapshotHandle.dispose()
for (vW in runtime.windows.toList()) runtime.scheduleDestroy(vW)
runtime.reapDestroyed()
com.compose.sdl.graphics.NativeReleaseQueue.drain()
appComposition.dispose()
appRecomposer.cancel()
appRecomposeJob.cancelAndJoin()
}
mainDispatcher.close()
@OptIn(ExperimentalCoroutinesApi::class)
Dispatchers.resetMain()
SDL_Quit()
}
/** Single-window compatibility wrapper - the pre-multi-window entry point.
Closing the window exits the app, exactly as before. */
fun nativeComposeWindow(
title: String = "ComposeNativeSDL3",
width: Int = 800,
height: Int = 600,
gpu: GpuMode = GpuMode.Auto,
icon: AppWindowIcon? = null,
undecorated: Boolean = false,
transparent: Boolean = false,
resizable: Boolean = true,
enabled: Boolean = true,
focusable: Boolean = true,
alwaysOnTop: Boolean = false,
onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
onKeyEvent: (KeyEvent) -> Boolean = { false },
onFrame: ((backend: RenderBackend, frameIndex: Int) -> Boolean)? = null,
content: @Composable ComposeWindowScope.() -> Unit,
) {
nativeComposeApp {
Window(
onCloseRequest = { exitApplication() },
title = title,
width = width,
height = height,
gpu = gpu,
icon = icon,
undecorated = undecorated,
transparent = transparent,
resizable = resizable,
enabled = enabled,
focusable = focusable,
alwaysOnTop = alwaysOnTop,
onPreviewKeyEvent = onPreviewKeyEvent,
onKeyEvent = onKeyEvent,
onFrame = onFrame,
content = content,
)
}
}
@@ -9,65 +9,65 @@ import sdl3.SDL_GetTicks
// MARK: FrameProfiler // MARK: FrameProfiler
// ================== // ==================
/** CDN_PROFILE=1 — per-phase timings, printed every ~2s of rendered frames. /** CDN_PROFILE=1 - per-phase timings, printed every ~2s of rendered frames.
A named-phase SINGLETON so both the main loop (events / app / pump / render) A named-phase SINGLETON so both the main loop (events / app / pump / render)
AND renderFrame's sub-steps (render.layout / render.draw / render.present) AND renderFrame's sub-steps (render.layout / render.draw / render.present)
report into one line. `mark()` resets the stopwatch; `phase(name)` charges report into one line. `mark()` resets the stopwatch; `phase(name)` charges
the elapsed since the last mark/phase to that name. Measure first, optimize the elapsed since the last mark/phase to that name. Measure first, optimize
second — see ROADMAP.md. */ second - see ROADMAP.md. */
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
internal object FrameProfiler { internal object FrameProfiler {
// null until first checked; then true/false for the run's lifetime. // null until first checked; then true/false for the run's lifetime.
private var fEnabled: Boolean? = null private var mEnabled: Boolean? = null
// Output file — resolved once from CDN_PROFILE. Writing to a file (not // Output file - resolved once from CDN_PROFILE. Writing to a file (not
// stdout) lets GUI-subsystem apps (the demo links --subsystem,windows, so // stdout) lets GUI-subsystem apps (the demo links --subsystem,windows, so
// it has no console) be profiled too. CDN_PROFILE=1 → "cdn_profile.log" in // it has no console) be profiled too. CDN_PROFILE=1 → "cdn_profile.log" in
// the cwd; CDN_PROFILE=<path> → that path. // the cwd; CDN_PROFILE=<path> → that path.
private var fPath: String = "cdn_profile.log" private var mPath: String = "cdn_profile.log"
val enabled: Boolean val enabled: Boolean
get() = fEnabled ?: run { get() = mEnabled ?: run {
val vEnv = platform.posix.getenv("CDN_PROFILE")?.toKString() val vEnv = platform.posix.getenv("CDN_PROFILE")?.toKString()
if (vEnv != null && vEnv != "1" && vEnv.isNotEmpty()) fPath = vEnv if (vEnv != null && vEnv != "1" && vEnv.isNotEmpty()) mPath = vEnv
(vEnv != null).also { fEnabled = it } (vEnv != null).also { mEnabled = it }
} }
private val fFreq = SDL_GetPerformanceFrequency().toDouble() private val mFreq = SDL_GetPerformanceFrequency().toDouble()
// Insertion-ordered so the printed line follows the call order. // Insertion-ordered so the printed line follows the call order.
private val fSum = LinkedHashMap<String, Double>() private val mSum = LinkedHashMap<String, Double>()
private val fMax = LinkedHashMap<String, Double>() private val mMax = LinkedHashMap<String, Double>()
private var fFrames = 0 private var mFrames = 0
private var fLastPrintMs = SDL_GetTicks() private var mLastPrintMs = SDL_GetTicks()
private var fMark = 0uL private var mMark = 0uL
fun mark() { if (enabled) fMark = SDL_GetPerformanceCounter() } fun mark() { if (enabled) mMark = SDL_GetPerformanceCounter() }
fun phase(inName: String) { fun phase(inName: String) {
if (!enabled) return if (!enabled) return
val vNow = SDL_GetPerformanceCounter() val vNow = SDL_GetPerformanceCounter()
val vMs = (vNow - fMark).toDouble() * 1000.0 / fFreq val vMs = (vNow - mMark).toDouble() * 1000.0 / mFreq
fSum[inName] = (fSum[inName] ?: 0.0) + vMs mSum[inName] = (mSum[inName] ?: 0.0) + vMs
if (vMs > (fMax[inName] ?: 0.0)) fMax[inName] = vMs if (vMs > (mMax[inName] ?: 0.0)) mMax[inName] = vMs
fMark = vNow mMark = vNow
} }
fun frameDone(inRendered: Boolean) { fun frameDone(inRendered: Boolean) {
if (!enabled) return if (!enabled) return
if (inRendered) fFrames++ if (inRendered) mFrames++
val vNowMs = SDL_GetTicks() val vNowMs = SDL_GetTicks()
if (vNowMs - fLastPrintMs >= 2000u && fFrames > 0) { if (vNowMs - mLastPrintMs >= 2000u && mFrames > 0) {
val vParts = fSum.keys.map { vName -> val vParts = mSum.keys.map { vName ->
val vAvg = (fSum[vName] ?: 0.0) / fFrames val vAvg = (mSum[vName] ?: 0.0) / mFrames
"$vName=${(vAvg * 100).toInt() / 100.0}/${((fMax[vName] ?: 0.0) * 100).toInt() / 100.0}ms" "$vName=${(vAvg * 100).toInt() / 100.0}/${((mMax[vName] ?: 0.0) * 100).toInt() / 100.0}ms"
} }
val vLine = "[profile] frames=$fFrames avg/max " + vParts.joinToString(" ") + "\n" val vLine = "[profile] frames=$mFrames avg/max " + vParts.joinToString(" ") + "\n"
val vFile = platform.posix.fopen(fPath, "a") val vFile = platform.posix.fopen(mPath, "a")
if (vFile != null) { if (vFile != null) {
platform.posix.fputs(vLine, vFile) platform.posix.fputs(vLine, vFile)
platform.posix.fclose(vFile) platform.posix.fclose(vFile)
} }
fSum.clear(); fMax.clear() mSum.clear(); mMax.clear()
fFrames = 0 mFrames = 0
fLastPrintMs = vNowMs mLastPrintMs = vNowMs
} }
} }
} }
@@ -0,0 +1,83 @@
@file:OptIn(
androidx.compose.ui.InternalComposeUiApi::class,
androidx.compose.runtime.InternalComposeApi::class,
)
package com.compose.sdl
import kotlinx.cinterop.COpaquePointer
import kotlinx.cinterop.reinterpret
import kotlinx.cinterop.pointed
import kotlinx.coroutines.CancellationException
import sdl3.SDL_GetCurrentDisplayMode
import sdl3.SDL_GetDisplayForWindow
import sdl3.SDL_GetTicks
import sdl3.SDL_GetTicksNS
// ==================
// MARK: Deterministic frame timing + probe/screenshot support
// ==================
/** Set BEFORE nativeComposeApp/nativeComposeWindow: every window's composition then runs
under an InfiniteAnimationPolicy that CANCELS infinite animations, so
rememberInfiniteTransition & co. freeze at their initial value - the same mechanism
upstream's test rules use. Screenshot/parity runs enable it so looping screens can
reach quiescence and capture deterministically. */
var disableInfiniteAnimations: Boolean = false
/** The cancelling policy: a coroutine that ends in CancellationException counts as
cancelled (not failed), so only the animation coroutine stops - nothing propagates. */
internal object CancelInfiniteAnimationsPolicy : androidx.compose.ui.platform.InfiniteAnimationPolicy {
override suspend fun <R> onInfiniteOperation(block: suspend () -> R): R =
throw CancellationException("infinite animations are disabled (disableInfiniteAnimations)")
}
/** Set BEFORE nativeComposeApp/nativeComposeWindow: the composition + animation frame
clocks advance a VIRTUAL 16.6ms per main-loop iteration instead of reading SDL's
real-time ticks - the native mirror of the JVM parity leg's render(nanos) stepping.
Animations then progress by exact per-frame deltas, so anything time-raced (e.g. a
bring-into-view scroll interrupted mid-flight) resolves identically on every run and
screenshots become deterministic. Input timestamps and FPS stay on real time. */
var useVirtualFrameTime: Boolean = false
private var virtualFrameNanos = 0L
internal fun advanceVirtualFrame() {
if (useVirtualFrameTime) virtualFrameNanos += 16_666_667L
}
// Timestamp for the composition frame clocks (recomposer + withFrameNanos animations).
internal fun frameClockNanos(): Long =
if (useVirtualFrameTime) virtualFrameNanos else SDL_GetTicksNS().toLong()
// Timestamp for the owner's node-animation clock - real path keeps the pre-existing
// ms-resolution SDL_GetTicks base so non-screenshot behaviour is bit-for-bit unchanged.
internal fun animationClockNanos(): Long =
if (useVirtualFrameTime) virtualFrameNanos else SDL_GetTicks().toLong() * 1_000_000L
// The window currently inside renderFrame - the loop is single-threaded, so a plain
// var is enough for onFrame probes to address "the window I'm being called for".
internal var renderingWindow: WindowInstance? = null
/** From inside an onFrame callback: true while the just-rendered window still has pending
work (recomposition, layout/draw invalidation, or an animation awaiting the next frame).
Screenshot probes capture once this stays false for a few consecutive frames instead of
at a fixed frame count. Outside onFrame it answers false. */
fun windowHasInvalidations(): Boolean = renderingWindow?.hasInvalidations() ?: false
/** Trigger a Kotlin/Native GC so Cleaner-managed renderer resources release
their native memory (see the main loop's native-memory nudge). */
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class)
internal fun collectNativeGarbage() = kotlin.native.runtime.GC.collect()
/** Frame interval (ms) of the window's current display, for the non-vsync
fallback pacing (SDL_Delay). Falls back to 16ms (~60Hz) when the mode can't
be read, so a 144Hz panel on a non-vsync path isn't capped to 60. */
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
internal fun displayFrameDelayMs(window: COpaquePointer?): UInt {
val vWindow = window ?: return 16u
val vDisplay = SDL_GetDisplayForWindow(vWindow.reinterpret())
val vMode = SDL_GetCurrentDisplayMode(vDisplay) ?: return 16u
val vHz = vMode.pointed.refresh_rate
return if (vHz > 0f) (1000f / vHz).toUInt().coerceAtLeast(1u) else 16u
}
@@ -6,7 +6,7 @@ import sdl3.SDL_GetTicksNS
/** MonotonicFrameClock for the SDL main loop, with upstream frame semantics: /** MonotonicFrameClock for the SDL main loop, with upstream frame semantics:
- withFrameNanos ALWAYS suspends until the NEXT sendFrame — never completes - withFrameNanos ALWAYS suspends until the NEXT sendFrame - never completes
with a stale frame. Deferred animations (AnimatedVisibility's enter/exit with a stale frame. Deferred animations (AnimatedVisibility's enter/exit
size + fade, SharedTransition bounds) are registered during the size + fade, SharedTransition bounds) are registered during the
measure/draw pass AFTER the recomposition that starts a Transition; the measure/draw pass AFTER the recomposition that starts a Transition; the
@@ -20,7 +20,7 @@ import sdl3.SDL_GetTicksNS
Both come from delegating to the runtime's BroadcastFrameClock, which is Both come from delegating to the runtime's BroadcastFrameClock, which is
exactly upstream's dispatch structure. */ exactly upstream's dispatch structure. */
internal class SDL3FrameClock : MonotonicFrameClock { internal class Sdl3FrameClock : MonotonicFrameClock {
private val broadcast = BroadcastFrameClock() private val broadcast = BroadcastFrameClock()
override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R = override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
@@ -31,7 +31,7 @@ internal class SDL3FrameClock : MonotonicFrameClock {
} }
// Coroutines (recomposer / composition animations) currently suspended in withFrameNanos // Coroutines (recomposer / composition animations) currently suspended in withFrameNanos
// awaiting the next sendFrame — the "an animation is still running" half of the window's // awaiting the next sendFrame - the "an animation is still running" half of the window's
// quiescence signal (see WindowInstance.hasInvalidations). // quiescence signal (see WindowInstance.hasInvalidations).
val hasAwaiters: Boolean get() = broadcast.hasAwaiters val hasAwaiters: Boolean get() = broadcast.hasAwaiters
} }
@@ -12,7 +12,7 @@ import sdl3.SDL_GetCurrentThreadID
/** Single-threaded Main dispatcher driven by the SDL3 main loop. Kotlin/Native /** Single-threaded Main dispatcher driven by the SDL3 main loop. Kotlin/Native
ships no Dispatchers.Main on Linux/Windows targets (and the Darwin one ships no Dispatchers.Main on Linux/Windows targets (and the Darwin one
posts to GCD's main queue, which only drains when the run loop pumps — posts to GCD's main queue, which only drains when the run loop pumps -
our SDL_Delay-based loop doesn't reliably pump it). nativeComposeWindow our SDL_Delay-based loop doesn't reliably pump it). nativeComposeWindow
installs this dispatcher via kotlinx.coroutines.test.setMain at startup installs this dispatcher via kotlinx.coroutines.test.setMain at startup
and resets it on shutdown, so app code can withContext(Dispatchers.Main) and resets it on shutdown, so app code can withContext(Dispatchers.Main)
@@ -24,28 +24,28 @@ import sdl3.SDL_GetCurrentThreadID
(Dispatchers.IO, Default, etc.) are safe. (Dispatchers.IO, Default, etc.) are safe.
`immediate` is a REAL immediate dispatcher: its isDispatchNeeded() returns `immediate` is a REAL immediate dispatcher: its isDispatchNeeded() returns
false when already on the SDL main thread, so work runs inline — matching false when already on the SDL main thread, so work runs inline - matching
Android's Main.immediate / Swing's EDT semantics. This is load-bearing: Android's Main.immediate / Swing's EDT semantics. This is load-bearing:
androidx.lifecycle's KMP LifecycleRegistry enforces main-thread access by androidx.lifecycle's KMP LifecycleRegistry enforces main-thread access by
round-tripping through Dispatchers.Main.immediate (MainDispatcherChecker); round-tripping through Dispatchers.Main.immediate (MainDispatcherChecker);
with a queue-only Main that trip can never complete from the main thread with a queue-only Main that trip can never complete from the main thread
while the loop is inside setContent, deadlocking the app at 0% CPU (this while the loop is inside setContent, deadlocking the app at 0% CPU (this
froze Navigation3's NavDisplay — rememberLifecycleOwner — on mingwX64, froze Navigation3's NavDisplay - rememberLifecycleOwner - on mingwX64,
see NAV_FIX.md). The BASE dispatcher intentionally KEEPS always-queue see NAV_FIX.md). The BASE dispatcher intentionally KEEPS always-queue
semantics so LaunchedEffect / recomposer ordering is unchanged: state semantics so LaunchedEffect / recomposer ordering is unchanged: state
writes land at the loop's drainPending(), observable on the same frame. */ writes land at the loop's drainPending(), observable on the same frame. */
internal class Sdl3MainDispatcher : MainCoroutineDispatcher() { internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
private val fQueue = Channel<Runnable>(Channel.UNLIMITED) private val mQueue = Channel<Runnable>(Channel.UNLIMITED)
// The SDL main thread — the dispatcher is constructed by nativeComposeApp // The SDL main thread - the dispatcher is constructed by nativeComposeApp
// on the thread that then runs the loop. // on the thread that then runs the loop.
private val fMainThreadId = SDL_GetCurrentThreadID() private val mMainThreadId = SDL_GetCurrentThreadID()
override val immediate: MainCoroutineDispatcher = ImmediateDispatcher() override val immediate: MainCoroutineDispatcher = ImmediateDispatcher()
override fun dispatch(context: CoroutineContext, block: Runnable) { override fun dispatch(context: CoroutineContext, block: Runnable) {
fQueue.trySend(block) mQueue.trySend(block)
} }
/** Drains all pending tasks. Called from the SDL3 main loop once per /** Drains all pending tasks. Called from the SDL3 main loop once per
@@ -54,7 +54,7 @@ internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
composition. */ composition. */
fun drainPending() { fun drainPending() {
while (true) { while (true) {
val vTask = fQueue.tryReceive().getOrNull() ?: break val vTask = mQueue.tryReceive().getOrNull() ?: break
runCatching { vTask.run() }.onFailure { vErr -> runCatching { vTask.run() }.onFailure { vErr ->
println("Sdl3MainDispatcher: dispatched task threw: ${vErr.message}") println("Sdl3MainDispatcher: dispatched task threw: ${vErr.message}")
} }
@@ -64,7 +64,7 @@ internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
/** Closes the queue. After this, dispatch() drops new posts silently. /** Closes the queue. After this, dispatch() drops new posts silently.
Called from nativeComposeWindow as part of shutdown. */ Called from nativeComposeWindow as part of shutdown. */
fun close() { fun close() {
fQueue.close() mQueue.close()
} }
override fun toString(): String = "Sdl3MainDispatcher" override fun toString(): String = "Sdl3MainDispatcher"
@@ -75,10 +75,10 @@ internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
override val immediate: MainCoroutineDispatcher get() = this override val immediate: MainCoroutineDispatcher get() = this
override fun isDispatchNeeded(context: CoroutineContext): Boolean = override fun isDispatchNeeded(context: CoroutineContext): Boolean =
SDL_GetCurrentThreadID() != fMainThreadId SDL_GetCurrentThreadID() != mMainThreadId
override fun dispatch(context: CoroutineContext, block: Runnable) { override fun dispatch(context: CoroutineContext, block: Runnable) {
fQueue.trySend(block) mQueue.trySend(block)
} }
override fun toString(): String = "Sdl3MainDispatcher.immediate" override fun toString(): String = "Sdl3MainDispatcher.immediate"
@@ -13,11 +13,11 @@ import androidx.lifecycle.enableSavedStateHandles
default ViewModel factory), so viewModel(), SavedStateHandle and default ViewModel factory), so viewModel(), SavedStateHandle and
rememberSaveable-backed registries all resolve against the WINDOW scope. rememberSaveable-backed registries all resolve against the WINDOW scope.
The lifecycle registry uses createUnsafe (no main-thread enforcement) — The lifecycle registry uses createUnsafe (no main-thread enforcement) -
same as the root owner this replaces; the SDL loop is single-threaded same as the root owner this replaces; the SDL loop is single-threaded
anyway. RESUMED from construction; destroy() moves to DESTROYED and clears anyway. RESUMED from construction; destroy() moves to DESTROYED and clears
the ViewModelStore (onCleared runs). SavedState restores from nothing (no the ViewModelStore (onCleared runs). SavedState restores from nothing (no
process-death persistence on desktop — upstream desktop passes null too). */ process-death persistence on desktop - upstream desktop passes null too). */
internal class WindowArchitectureOwner : internal class WindowArchitectureOwner :
androidx.lifecycle.LifecycleOwner, androidx.lifecycle.LifecycleOwner,
androidx.lifecycle.ViewModelStoreOwner, androidx.lifecycle.ViewModelStoreOwner,
@@ -41,13 +41,13 @@ internal class WindowArchitectureOwner :
init { init {
savedStateController.performAttach() savedStateController.performAttach()
savedStateController.performRestore(null) savedStateController.performRestore(null)
// SavedStateHandle support for WINDOW-scoped ViewModels — must run while // SavedStateHandle support for WINDOW-scoped ViewModels - must run while
// the lifecycle is still ≤ CREATED; upstream desktop's ComposeContainer // the lifecycle is still ≤ CREATED; upstream desktop's ComposeContainer
// calls this at the same point. With it, `viewModel { ... }` against the // calls this at the same point. With it, `viewModel { ... }` against the
// window owner (the activityViewModels() analog) can take a // window owner (the activityViewModels() analog) can take a
// SavedStateHandle instead of needing a saved-state-less child owner. // SavedStateHandle instead of needing a saved-state-less child owner.
enableSavedStateHandles() enableSavedStateHandles()
// CREATED (not RESUMED) until the first composition is done — code that // CREATED (not RESUMED) until the first composition is done - code that
// runs enableSavedStateHandles() during composition (nav3's decorators, // runs enableSavedStateHandles() during composition (nav3's decorators,
// rememberViewModelStoreOwner, …) requires INITIALIZED/CREATED, and // rememberViewModelStoreOwner, …) requires INITIALIZED/CREATED, and
// upstream desktop windows likewise compose first and resume after. // upstream desktop windows likewise compose first and resume after.
@@ -55,7 +55,7 @@ internal class WindowArchitectureOwner :
} }
/** Focus/visibility-driven state (see WindowInstance.onActivationEvent). /** Focus/visibility-driven state (see WindowInstance.onActivationEvent).
Ignored once destroyed — a stray SDL event during teardown must not Ignored once destroyed - a stray SDL event during teardown must not
resurrect the registry. */ resurrect the registry. */
fun setLifecycleState(inState: androidx.lifecycle.Lifecycle.State) { fun setLifecycleState(inState: androidx.lifecycle.Lifecycle.State) {
if (lifecycle.currentState != androidx.lifecycle.Lifecycle.State.DESTROYED && if (lifecycle.currentState != androidx.lifecycle.Lifecycle.State.DESTROYED &&
@@ -0,0 +1,31 @@
package com.compose.sdl
// ==================
// MARK: WindowAttributes
// ==================
/**
The Compose Desktop `Window()` attributes this port supports, bundled so they
travel as one value from the composable down to [WindowInstance] instead of as
six positional booleans.
[transparent] is creation-only: SDL needs "SDL_WINDOW_TRANSPARENT" at
"SDL_CreateWindow" time and offers no setter, so changing it after the window
exists has no effect. The rest are re-applied whenever "Window()" recomposes
with a new value.
*/
internal data class WindowAttributes(
/** Hides the title bar and border (`SDL_WINDOW_BORDERLESS`). */
val undecorated: Boolean = false,
/** Per-pixel alpha for the window surface. CREATION-ONLY - see above. */
val transparent: Boolean = false,
/** Whether the user can resize the window. */
val resizable: Boolean = true,
/** False drops input (pointer / key / text / wheel / drop) while the window
keeps rendering - Compose Desktop's `enabled = false` behaviour. */
val enabled: Boolean = true,
/** False stops the window taking keyboard focus when clicked or raised. */
val focusable: Boolean = true,
/** Keeps the window above other windows. */
val alwaysOnTop: Boolean = false,
)
@@ -7,19 +7,14 @@ package com.compose.sdl
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.AbstractApplier
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Recomposer import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerEventType
import com.compose.sdl.node.ComposeRootHost import com.compose.sdl.node.ComposeRootHost
@@ -27,457 +22,17 @@ import com.compose.sdl.res.currentImageLoader
import com.compose.sdl.window.LocalPopupHost import com.compose.sdl.window.LocalPopupHost
import com.compose.sdl.window.PopupLayer import com.compose.sdl.window.PopupLayer
import com.compose.sdl.window.createPopupHostState import com.compose.sdl.window.createPopupHostState
import kotlinx.coroutines.*
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import kotlinx.cinterop.toKString
import kotlinx.cinterop.reinterpret
import kotlinx.cinterop.alloc import kotlinx.cinterop.alloc
import kotlinx.cinterop.ptr import kotlinx.cinterop.ptr
import kotlinx.cinterop.COpaquePointer import kotlinx.cinterop.reinterpret
import kotlinx.cinterop.pointed import kotlinx.coroutines.CoroutineScope
import sdl3.SDL_Delay import kotlinx.coroutines.Job
import sdl3.SDL_GetCurrentDisplayMode import kotlinx.coroutines.launch
import sdl3.SDL_GetDisplayForWindow
import sdl3.SDL_GetPerformanceCounter
import sdl3.SDL_GetPerformanceFrequency
import sdl3.SDL_GetTicks import sdl3.SDL_GetTicks
import sdl3.SDL_GetTicksNS
import sdl3.SDL_Quit
import sdl3.SDL_SetWindowTitle import sdl3.SDL_SetWindowTitle
import sdl3.SDL_WaitEventTimeout
// ================== // ==================
// MARK: Application entry — nativeComposeApp + Window() // MARK: WindowInstance - one SDL window + renderer + composition
// ==================
/**
Multi-window entry point, shaped like Compose Desktop's `application {}`:
nativeComposeApp {
var showTools by remember { mutableStateOf(false) }
Window(onCloseRequest = ::exitApplication, title = "Main") { MainUi(onTools = { showTools = true }) }
if (showTools) Window(onCloseRequest = { showTools = false }, title = "Tools") { ToolsUi() }
}
The app CONTENT is itself a composition (no UI tree — a Unit applier): each
`Window(...)` call materialises an SDL window + renderer + root host + its own
recomposer/composition, and leaving the composition (state flips to false)
tears the window down. One main loop pumps the shared SDL event queue and
routes events per SDL window id.
Per-window pumping order matters: the render-bridge globals
(currentImageLoader / viewport) are per-renderer, so the
loop installs a window's globals before recomposing / laying out / drawing it.
`nativeComposeWindow(...)` remains as the single-window wrapper (all demo
probes and apidemo ride it unchanged).
*/
interface ApplicationScope {
/** Requests the main loop to exit after the current iteration (all windows
are torn down on the way out). */
fun exitApplication()
}
/** Declares one native window for as long as this composable stays in the app
composition. `onCloseRequest` fires when the user asks the window to close
(OS close button, or `window.close()` from content) — remove the state that
composes this Window to actually close it, or call exitApplication(). */
@Composable
fun ApplicationScope.Window(
onCloseRequest: () -> Unit,
title: String = "ComposeNativeSDL3",
width: Int = 800,
height: Int = 600,
gpu: GpuMode = GpuMode.Auto,
icon: AppWindowIcon? = null,
onFrame: ((backend: RenderBackend, frameIndex: Int) -> Boolean)? = null,
content: @Composable ComposeWindowScope.() -> Unit,
) {
val vScope = this as? ApplicationScopeImpl
?: error("Window() must be called inside nativeComposeApp { ... }")
val vContent = rememberUpdatedState(content)
val vClose = rememberUpdatedState(onCloseRequest)
val vWindow = remember {
vScope.runtime.createWindow(
inTitle = title,
inWidth = width,
inHeight = height,
inGpu = gpu,
inIcon = icon,
inOnFrame = onFrame,
// The window composition reads the holder each recomposition, so
// updated content lambdas propagate without recreating the window.
inContent = { vContent.value },
).also { it.onCloseRequest = { vClose.value.invoke() } }
}
SideEffect {
if (vWindow.facade.title != title) vWindow.facade.setTitle(title)
}
DisposableEffect(Unit) {
onDispose { vScope.runtime.scheduleDestroy(vWindow) }
}
}
/** Boots SDL + the Compose runtime, runs `content` as the application
composition, and drives every declared Window until exitApplication() or
the last window closes. */
fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
// Install the Main dispatcher BEFORE any window/owner exists: ComposeOwner
// captures Dispatchers.Main eagerly for its per-node coroutine scopes.
val mainDispatcher = Sdl3MainDispatcher()
@OptIn(ExperimentalCoroutinesApi::class)
Dispatchers.setMain(mainDispatcher)
val runtime = AppRuntime()
runBlocking {
runtime.scope = this
// App-level composition: no UI tree — it only declares Window()s.
val appClock = SDL3FrameClock()
val appRecomposer = Recomposer(coroutineContext + appClock)
// The clock must be in the collector's context — runRecomposeAndApplyChanges
// awaits parent frames through it.
val appRecomposeJob = launch(appClock) { appRecomposer.runRecomposeAndApplyChanges() }
val appComposition = Composition(UnitApplier(), appRecomposer)
val appScope = ApplicationScopeImpl(runtime)
val snapshotHandle = Snapshot.registerGlobalWriteObserver {
// Only SCHEDULE a frame here; the apply is coalesced to once per loop
// iteration (top of loop + before each pump + before layout in
// renderFrame) instead of walking every snapshot observer on every
// individual state write. Mirrors upstream GlobalSnapshotManager,
// which schedules rather than applying inline.
runtime.markAllNeedFrame()
}
appComposition.setContent { appScope.content() }
// Compose the initial Window() declarations before entering the loop.
appClock.sendFrame(frameClockNanos())
yield()
// ============
// Main loop
var vGcLastTicks = SDL_GetTicks()
var vRenderedSinceGc = false
while (!runtime.exitRequested) {
FrameProfiler.mark()
// One virtual 16.6ms tick per loop iteration (no-op unless useVirtualFrameTime) —
// all windows' clocks share the same timestamp this iteration.
advanceVirtualFrame()
Snapshot.sendApplyNotifications()
// ============
// Events — one shared SDL queue, routed by window id.
val vEvents = pollEvents()
for (vEvent in vEvents) {
when (vEvent) {
is AppEvent.Quit -> {
// Platform quit = close request on every window; any
// veto (setOnCloseRequest returning false) keeps the
// app alive. No windows at all → plain exit.
if (runtime.windows.isEmpty()) runtime.exitRequested = true
else for (vW in runtime.windows.toList()) vW.requestClose()
}
is AppEvent.WindowClose -> runtime.windowFor(vEvent.windowId)?.requestClose()
is AppEvent.WindowResized -> runtime.windowFor(vEvent.windowId)?.onResizedEvent()
is AppEvent.RedrawNeeded -> runtime.windowFor(vEvent.windowId)?.let { it.needsFrame = true }
is AppEvent.SystemThemeChanged -> for (vW in runtime.windows) vW.onSystemThemeChanged()
is AppEvent.WindowActivation -> runtime.windowFor(vEvent.windowId)?.onActivationEvent(vEvent)
is AppEvent.Pointer -> runtime.windowFor(vEvent.windowId)?.onPointerEvent(vEvent)
is AppEvent.PointerExit -> runtime.windowFor(vEvent.windowId)?.onPointerExit()
is AppEvent.MouseWheel -> runtime.windowFor(vEvent.windowId)?.onWheelEvent(vEvent)
is AppEvent.Key -> runtime.windowFor(vEvent.windowId)?.onKeyEvent(vEvent)
is AppEvent.TextInput -> runtime.windowFor(vEvent.windowId)?.onTextInputEvent(vEvent)
is AppEvent.TextEditing -> runtime.windowFor(vEvent.windowId)?.onTextEditingEvent(vEvent)
is AppEvent.Drop -> runtime.windowFor(vEvent.windowId)?.onDropEvent(vEvent)
}
}
mainDispatcher.drainPending()
FrameProfiler.phase("events")
// ============
// App composition pump — Window()s may appear / disappear here.
Snapshot.sendApplyNotifications()
appClock.sendFrame(frameClockNanos())
yield()
runtime.reapDestroyed()
// Window content can flip window.close() / probes can end the app.
for (vW in runtime.windows.toList()) {
if (vW.facade.isCloseRequested && !vW.closeDispatched) {
vW.closeDispatched = true
vW.onCloseRequest()
}
}
// Exit when the last window is gone (after at least one existed).
if (runtime.hadWindow && runtime.windows.isEmpty()) runtime.exitRequested = true
if (runtime.exitRequested) break
FrameProfiler.phase("app")
// ============
// Per-window pump + render.
var vAnyRendered = false
var vAllVsync = true
// Non-vsync fallback cap: the shortest frame interval among the
// rendered non-vsync windows' displays (60Hz default). Only used when
// no window is vsync-paced (Software renderer / vsync unavailable).
var vFallbackDelayMs = 16u
val vAppPending = appRecomposer.hasPendingWork
for (vW in runtime.windows.toList()) {
vW.installGlobals()
vW.host.sendAnimationFrame(animationClockNanos())
mainDispatcher.drainPending()
Snapshot.sendApplyNotifications()
vW.frameClock.sendFrame(frameClockNanos())
yield()
FrameProfiler.phase("pump")
if (vW.shouldRender()) {
vW.renderFrame()
vAnyRendered = true
if (!vW.backend.vsyncEnabled) {
vAllVsync = false
vFallbackDelayMs = minOf(vFallbackDelayMs, displayFrameDelayMs(vW.backend.window))
}
}
FrameProfiler.phase("render")
}
runtime.reapDestroyed()
FrameProfiler.frameDone(vAnyRendered)
// ============
// Drain deferred native-resource disposals on the MAIN thread —
// textures/surfaces whose owner closed them or whose Cleaner fired
// on a GC worker enqueue here (SDL calls aren't thread-safe). This
// is the ownership path that makes the GC nudge below a mere
// backstop (ROADMAP.md item 1).
com.compose.sdl.graphics.NativeReleaseQueue.drain()
// ============
// Pace / idle-skip.
if (vAnyRendered) {
SDL_Delay(if (vAllVsync) 1u else vFallbackDelayMs)
} else if (!vAppPending) {
// Nothing invalidated anywhere: block for events instead of
// spinning. A real input/redraw event wakes this immediately; the
// timeout only bounds how often async work (dispatcher/timers) is
// re-checked while truly idle, so keep it coarse to cut wakeups.
SDL_WaitEventTimeout(null, 100)
for (vW in runtime.windows) vW.resetFpsWindow()
} else {
// App composition has pending work but nothing rendered this
// iteration — yield briefly instead of spinning at full speed
// until the composition settles into a renderable state.
SDL_Delay(1u)
}
// ============
// Native-memory nudge. Renderer resources (Skia surfaces / images /
// fonts, SDL textures) are freed by Cleaners that only run when the
// Kotlin/Native GC collects — and a Compose app's Kotlin heap is
// small enough that the allocation-driven scheduler can starve them
// for minutes while the NATIVE heap balloons (issue #2: memory
// "never released" while navigating). Collect periodically, only
// after rendering activity, between frames; costs ~ms at these
// heap sizes and keeps RSS tracking real usage.
if (vAnyRendered) vRenderedSinceGc = true
val vNowTicks = SDL_GetTicks()
if (vRenderedSinceGc && vNowTicks - vGcLastTicks >= 10_000uL) {
vGcLastTicks = vNowTicks
vRenderedSinceGc = false
collectNativeGarbage()
}
}
// ============
// Teardown
snapshotHandle.dispose()
for (vW in runtime.windows.toList()) runtime.scheduleDestroy(vW)
runtime.reapDestroyed()
com.compose.sdl.graphics.NativeReleaseQueue.drain()
appComposition.dispose()
appRecomposer.cancel()
appRecomposeJob.cancelAndJoin()
}
mainDispatcher.close()
@OptIn(ExperimentalCoroutinesApi::class)
Dispatchers.resetMain()
SDL_Quit()
}
// ==================
// MARK: Probe / screenshot support (render-to-quiescence)
// ==================
/** Set BEFORE nativeComposeApp/nativeComposeWindow: every window's composition then runs
under an InfiniteAnimationPolicy that CANCELS infinite animations, so
rememberInfiniteTransition & co. freeze at their initial value — the same mechanism
upstream's test rules use. Screenshot/parity runs enable it so looping screens can
reach quiescence and capture deterministically. */
var disableInfiniteAnimations: Boolean = false
/** The cancelling policy: a coroutine that ends in CancellationException counts as
cancelled (not failed), so only the animation coroutine stops — nothing propagates. */
private object CancelInfiniteAnimationsPolicy : androidx.compose.ui.platform.InfiniteAnimationPolicy {
override suspend fun <R> onInfiniteOperation(block: suspend () -> R): R =
throw CancellationException("infinite animations are disabled (disableInfiniteAnimations)")
}
/** Set BEFORE nativeComposeApp/nativeComposeWindow: the composition + animation frame
clocks advance a VIRTUAL 16.6ms per main-loop iteration instead of reading SDL's
real-time ticks — the native mirror of the JVM parity leg's render(nanos) stepping.
Animations then progress by exact per-frame deltas, so anything time-raced (e.g. a
bring-into-view scroll interrupted mid-flight) resolves identically on every run and
screenshots become deterministic. Input timestamps and FPS stay on real time. */
var useVirtualFrameTime: Boolean = false
private var virtualFrameNanos = 0L
private fun advanceVirtualFrame() {
if (useVirtualFrameTime) virtualFrameNanos += 16_666_667L
}
// Timestamp for the composition frame clocks (recomposer + withFrameNanos animations).
private fun frameClockNanos(): Long =
if (useVirtualFrameTime) virtualFrameNanos else SDL_GetTicksNS().toLong()
// Timestamp for the owner's node-animation clock — real path keeps the pre-existing
// ms-resolution SDL_GetTicks base so non-screenshot behaviour is bit-for-bit unchanged.
private fun animationClockNanos(): Long =
if (useVirtualFrameTime) virtualFrameNanos else SDL_GetTicks().toLong() * 1_000_000L
// The window currently inside renderFrame — the loop is single-threaded, so a plain
// var is enough for onFrame probes to address "the window I'm being called for".
private var renderingWindow: WindowInstance? = null
/** From inside an onFrame callback: true while the just-rendered window still has pending
work (recomposition, layout/draw invalidation, or an animation awaiting the next frame).
Screenshot probes capture once this stays false for a few consecutive frames instead of
at a fixed frame count. Outside onFrame it answers false. */
fun windowHasInvalidations(): Boolean = renderingWindow?.hasInvalidations() ?: false
/** Trigger a Kotlin/Native GC so Cleaner-managed renderer resources release
their native memory (see the main loop's native-memory nudge). */
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class)
private fun collectNativeGarbage() = kotlin.native.runtime.GC.collect()
/** Frame interval (ms) of the window's current display, for the non-vsync
fallback pacing (SDL_Delay). Falls back to 16ms (~60Hz) when the mode can't
be read, so a 144Hz panel on a non-vsync path isn't capped to 60. */
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
private fun displayFrameDelayMs(window: COpaquePointer?): UInt {
val vWindow = window ?: return 16u
val vDisplay = SDL_GetDisplayForWindow(vWindow.reinterpret())
val vMode = SDL_GetCurrentDisplayMode(vDisplay) ?: return 16u
val vHz = vMode.pointed.refresh_rate
return if (vHz > 0f) (1000f / vHz).toUInt().coerceAtLeast(1u) else 16u
}
/** Single-window compatibility wrapper — the pre-multi-window entry point.
Closing the window exits the app, exactly as before. */
fun nativeComposeWindow(
title: String = "ComposeNativeSDL3",
width: Int = 800,
height: Int = 600,
gpu: GpuMode = GpuMode.Auto,
icon: AppWindowIcon? = null,
onFrame: ((backend: RenderBackend, frameIndex: Int) -> Boolean)? = null,
content: @Composable ComposeWindowScope.() -> Unit,
) {
nativeComposeApp {
Window(
onCloseRequest = { exitApplication() },
title = title,
width = width,
height = height,
gpu = gpu,
icon = icon,
onFrame = onFrame,
content = content,
)
}
}
// ==================
// MARK: App runtime internals
// ==================
internal class ApplicationScopeImpl(val runtime: AppRuntime) : ApplicationScope {
override fun exitApplication() { runtime.exitRequested = true }
}
/** Registry + lifecycle for the live windows. Windows are created by the app
composition (Window()'s remember) and destroyed by the LOOP — DisposableEffect
onDispose only schedules, because teardown disposes a composition and that
must not run re-entrantly inside another composition's apply pass. */
internal class AppRuntime {
lateinit var scope: CoroutineScope
val windows = mutableListOf<WindowInstance>()
private val destroyQueue = mutableListOf<WindowInstance>()
var exitRequested = false
var hadWindow = false
fun windowFor(inId: UInt): WindowInstance? =
if (inId == 0u) windows.firstOrNull()
else windows.firstOrNull { it.backend.windowId == inId } ?: windows.firstOrNull()
fun markAllNeedFrame() {
for (vW in windows) vW.needsFrame = true
}
fun createWindow(
inTitle: String,
inWidth: Int,
inHeight: Int,
inGpu: GpuMode,
inIcon: AppWindowIcon?,
inOnFrame: ((RenderBackend, Int) -> Boolean)?,
inContent: () -> (@Composable ComposeWindowScope.() -> Unit),
): WindowInstance {
val vWindow = WindowInstance(inTitle, inWidth, inHeight, inGpu, inIcon, inOnFrame, inContent)
// A Window() was declared either way — the "exit when the last window
// is gone" rule must also fire when every declared window failed to
// initialise (otherwise the loop would spin forever with none).
hadWindow = true
if (vWindow.init(scope)) {
windows.add(vWindow)
} else {
println("nativeComposeApp: window '$inTitle' failed to initialise")
}
return vWindow
}
fun scheduleDestroy(inWindow: WindowInstance) {
if (inWindow in windows && inWindow !in destroyQueue) destroyQueue.add(inWindow)
}
fun reapDestroyed() {
while (destroyQueue.isNotEmpty()) {
val vW = destroyQueue.removeLast()
windows.remove(vW)
vW.destroy()
}
}
}
// No-op applier for the app-level composition — it emits no UI nodes, only
// side effects (each Window() manages a native window).
private class UnitApplier : AbstractApplier<Unit>(Unit) {
override fun insertTopDown(index: Int, instance: Unit) {}
override fun insertBottomUp(index: Int, instance: Unit) {}
override fun remove(index: Int, count: Int) {}
override fun move(from: Int, to: Int, count: Int) {}
override fun onClear() {}
}
// ==================
// MARK: WindowInstance — one SDL window + renderer + composition
// ================== // ==================
internal class WindowInstance( internal class WindowInstance(
@@ -486,7 +41,12 @@ internal class WindowInstance(
inHeight: Int, inHeight: Int,
inGpu: GpuMode, inGpu: GpuMode,
inIcon: AppWindowIcon?, inIcon: AppWindowIcon?,
private val attrs: WindowAttributes,
private val onFrame: ((RenderBackend, Int) -> Boolean)?, private val onFrame: ((RenderBackend, Int) -> Boolean)?,
// Window-level key handlers, read through holders so Window() recompositions
// with new lambdas take effect without recreating the window.
private val previewKeyHandler: () -> ((KeyEvent) -> Boolean),
private val keyHandler: () -> ((KeyEvent) -> Boolean),
private val contentHolder: () -> (@Composable ComposeWindowScope.() -> Unit), private val contentHolder: () -> (@Composable ComposeWindowScope.() -> Unit),
) { ) {
// Resolved renderer mode. `var` so init() can fall back GPU → CPU raster when // Resolved renderer mode. `var` so init() can fall back GPU → CPU raster when
@@ -496,7 +56,7 @@ internal class WindowInstance(
private val initialHeight = inHeight private val initialHeight = inHeight
private val icon = inIcon private val icon = inIcon
// Assigned by init() from the first backend that comes up (see makeBackend()). // Assigned by init() from the first backend that comes up (see makeBackend()).
lateinit var backend: SDL3Backend lateinit var backend: Sdl3Backend
private set private set
private var renderBackend: RenderBackend? = null private var renderBackend: RenderBackend? = null
lateinit var host: ComposeRootHost lateinit var host: ComposeRootHost
@@ -505,7 +65,7 @@ internal class WindowInstance(
private set private set
private var popupHost: com.compose.sdl.window.PopupHostState? = null private var popupHost: com.compose.sdl.window.PopupHostState? = null
val frameClock = SDL3FrameClock() val frameClock = Sdl3FrameClock()
private var recomposer: Recomposer? = null private var recomposer: Recomposer? = null
private var recomposeJob: Job? = null private var recomposeJob: Job? = null
private var composition: Composition? = null private var composition: Composition? = null
@@ -537,7 +97,7 @@ internal class WindowInstance(
private val backNavigationInput = BackNavigationInput() private val backNavigationInput = BackNavigationInput()
// WINDOW-scoped architecture-components owner (Lifecycle + ViewModelStore + // WINDOW-scoped architecture-components owner (Lifecycle + ViewModelStore +
// SavedStateRegistry) — the same trio upstream desktop's // SavedStateRegistry) - the same trio upstream desktop's
// DefaultArchitectureComponentsOwner supplies through the window // DefaultArchitectureComponentsOwner supplies through the window
// PlatformContext (compose/ui skikoMain PlatformOwnerProvider.skiko.kt). // PlatformContext (compose/ui skikoMain PlatformOwnerProvider.skiko.kt).
// viewModel(), SavedStateHandle plumbing and navigation3's // viewModel(), SavedStateHandle plumbing and navigation3's
@@ -563,19 +123,24 @@ internal class WindowInstance(
else -> androidx.lifecycle.Lifecycle.State.STARTED else -> androidx.lifecycle.Lifecycle.State.STARTED
} }
) )
// Shown / restored / focus-gained also invalidate the contents — keep // Shown / restored / focus-gained also invalidate the contents - keep
// the pre-lifecycle RedrawNeeded behaviour of these SDL events. // the pre-lifecycle RedrawNeeded behaviour of these SDL events.
needsFrame = true needsFrame = true
} }
// Creates + initialises an SDL3Backend for [mode] and its RenderBackend. On // Creates + initialises an Sdl3Backend for [mode] and its RenderBackend. On
// success commits `backend` and returns the renderer; else tears everything // success commits `backend` and returns the renderer; else tears everything
// down and returns null so the caller can retry with a different mode. // down and returns null so the caller can retry with a different mode.
private fun makeBackend(mode: GpuMode): RenderBackend? { private fun makeBackend(mode: GpuMode): RenderBackend? {
val vBackend = SDL3Backend( val vBackend = Sdl3Backend(
initialTitle, initialWidth, initialHeight, gpuMode = mode, initialTitle, initialWidth, initialHeight, gpuMode = mode,
iconLightResourcePaths = icon?.light ?: emptyList(), iconLightResourcePaths = icon?.light ?: emptyList(),
iconDarkResourcePaths = icon?.dark ?: emptyList(), iconDarkResourcePaths = icon?.dark ?: emptyList(),
undecorated = attrs.undecorated,
transparent = attrs.transparent,
resizable = attrs.resizable,
alwaysOnTop = attrs.alwaysOnTop,
focusable = attrs.focusable,
) )
if (!vBackend.init()) { if (!vBackend.init()) {
vBackend.destroy(inQuitSdl = false) vBackend.destroy(inQuitSdl = false)
@@ -597,9 +162,9 @@ internal class WindowInstance(
// can't come up (RDP / headless / missing GL driver), fall back once to // can't come up (RDP / headless / missing GL driver), fall back once to
// CPU raster (Software) so the window still opens instead of failing. // CPU raster (Software) so the window still opens instead of failing.
var vRender = makeBackend(gpuMode) var vRender = makeBackend(gpuMode)
if (vRender == null && gpuMode is GpuMode.Skia) { if (vRender == null && gpuMode.isGpuAccelerated) {
println("GPU renderer ($gpuMode) unavailable — falling back to CPU raster (Software)") println("GPU renderer ($gpuMode) unavailable - falling back to CPU raster (Software)")
gpuMode = GpuMode.Software gpuMode = GpuMode.CpuRaster
vRender = makeBackend(gpuMode) vRender = makeBackend(gpuMode)
} }
if (vRender == null) { if (vRender == null) {
@@ -611,10 +176,13 @@ internal class WindowInstance(
host = ComposeRootHost(inDensity = backend.pixelDensity) host = ComposeRootHost(inDensity = backend.pixelDensity)
host.attach() host.attach()
// A layer whose content changed (OwnedLayer.invalidate) schedules a frame // A layer whose content changed (OwnedLayer.invalidate) schedules a frame
// even when nothing else (recompose / relayout) is pending — retained layers // even when nothing else (recompose / relayout) is pending - retained layers
// need this so a draw-only state change still repaints. // need this so a draw-only state change still repaints.
host.setInvalidationCallback { needsFrame = true } host.setInvalidationCallback { needsFrame = true }
facade = ComposeNativeWindow(backend, gpuMode, initialTitle) facade = ComposeNativeWindow(backend, gpuMode, initialTitle)
// Mirror the creation flags into the facade so isResizable & co. read true
// from the start and a later Window() update only fires on a real change.
applyAttributes(attrs)
navigationEventOwner.navigationEventDispatcher.addInput(backNavigationInput) navigationEventOwner.navigationEventDispatcher.addInput(backNavigationInput)
// The render-bridge globals must point at THIS window's renderer while // The render-bridge globals must point at THIS window's renderer while
@@ -622,7 +190,7 @@ internal class WindowInstance(
// setContent below). // setContent below).
installGlobals() installGlobals()
// Effect context for this window's composition — the screenshot flag injects the // Effect context for this window's composition - the screenshot flag injects the
// infinite-animation-cancelling policy (see disableInfiniteAnimations above). // infinite-animation-cancelling policy (see disableInfiniteAnimations above).
var vEffectContext = inScope.coroutineContext + frameClock var vEffectContext = inScope.coroutineContext + frameClock
if (disableInfiniteAnimations) vEffectContext += CancelInfiniteAnimationsPolicy if (disableInfiniteAnimations) vEffectContext += CancelInfiniteAnimationsPolicy
@@ -646,7 +214,7 @@ internal class WindowInstance(
LocalComposeNativeWindow provides facade, LocalComposeNativeWindow provides facade,
LocalPopupHost provides vPopupHost, LocalPopupHost provides vPopupHost,
// Upstream vendored CompositionLocals.kt declares each of these as // Upstream vendored CompositionLocals.kt declares each of these as
// `staticCompositionLocalOf<T> { noLocalProvidedFor("…") }` — reading // `staticCompositionLocalOf<T> { noLocalProvidedFor("…") }` - reading
// one without a Provider throws. Seed them all from the ComposeOwner. // one without a Provider throws. Seed them all from the ComposeOwner.
androidx.compose.ui.platform.LocalDensity provides host.density, androidx.compose.ui.platform.LocalDensity provides host.density,
androidx.compose.ui.platform.LocalLayoutDirection provides host.layoutDirection, androidx.compose.ui.platform.LocalLayoutDirection provides host.layoutDirection,
@@ -663,7 +231,7 @@ internal class WindowInstance(
androidx.compose.ui.platform.LocalFontFamilyResolver provides androidx.compose.ui.platform.LocalFontFamilyResolver provides
com.compose.sdl.text.font.projectFontFamilyResolver, com.compose.sdl.text.font.projectFontFamilyResolver,
androidx.compose.ui.platform.LocalUriHandler provides vUriHandler, androidx.compose.ui.platform.LocalUriHandler provides vUriHandler,
// Desktop windows have no system bars / notch / IME insets — the // Desktop windows have no system bars / notch / IME insets - the
// interface's all-zero defaults are exactly right. // interface's all-zero defaults are exactly right.
androidx.compose.ui.platform.LocalPlatformWindowInsets provides androidx.compose.ui.platform.LocalPlatformWindowInsets provides
object : androidx.compose.ui.platform.PlatformWindowInsets {}, object : androidx.compose.ui.platform.PlatformWindowInsets {},
@@ -681,7 +249,7 @@ internal class WindowInstance(
androidx.lifecycle.compose.LocalLifecycleOwner provides architectureOwner, androidx.lifecycle.compose.LocalLifecycleOwner provides architectureOwner,
androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner provides architectureOwner, androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner provides architectureOwner,
androidx.savedstate.compose.LocalSavedStateRegistryOwner provides architectureOwner, androidx.savedstate.compose.LocalSavedStateRegistryOwner provides architectureOwner,
// Runtime-level host defaults — this window's navigation-event // Runtime-level host defaults - this window's navigation-event
// owner (SearchBar / sheets back plumbing). The viewmodel store // owner (SearchBar / sheets back plumbing). The viewmodel store
// is provided through the plain LocalViewModelStoreOwner above // is provided through the plain LocalViewModelStoreOwner above
// instead: the google lifecycle artifacts this port ships have no // instead: the google lifecycle artifacts this port ships have no
@@ -710,7 +278,7 @@ internal class WindowInstance(
} }
} }
} }
// First composition done (setContent is synchronous) — promote the // First composition done (setContent is synchronous) - promote the
// window lifecycle from CREATED to the focus/visibility-derived state. // window lifecycle from CREATED to the focus/visibility-derived state.
// Composition itself runs at CREATED so enableSavedStateHandles() // Composition itself runs at CREATED so enableSavedStateHandles()
// callers see a legal state, mirroring upstream desktop's // callers see a legal state, mirroring upstream desktop's
@@ -748,24 +316,25 @@ internal class WindowInstance(
// Event handling (loop-routed, per window) // Event handling (loop-routed, per window)
fun onPointerEvent(inEvent: AppEvent.Pointer) { fun onPointerEvent(inEvent: AppEvent.Pointer) {
if (!facade.isEnabled) return
needsFrame = true needsFrame = true
installGlobals() installGlobals()
val vType = when (inEvent.event.type) { val vType = when (inEvent.type) {
PointerEventType.Press -> 1 PointerEventType.Press -> 1
PointerEventType.Release -> 2 PointerEventType.Release -> 2
else -> 0 else -> 0
} }
val vBtn = when (inEvent.event.button) { val vBtn = when (inEvent.button) {
PointerButton.Secondary -> 1 PointerButton.Secondary -> 1
PointerButton.Tertiary -> 2 PointerButton.Tertiary -> 2
else -> 0 else -> 0
} }
// SDL3 delivers mouse coords in logical points on HiDPI — multiply by // SDL3 delivers mouse coords in logical points on HiDPI - multiply by
// DPR so hit-testing lands in the pixel space layout uses. // DPR so hit-testing lands in the pixel space layout uses.
val vDpr = backend.pixelDensity val vDpr = backend.pixelDensity
val vPx = inEvent.event.x * vDpr val vPx = inEvent.x * vDpr
val vPy = inEvent.event.y * vDpr val vPy = inEvent.y * vDpr
if (inEvent.event.type == PointerEventType.Press) { if (inEvent.type == PointerEventType.Press) {
popupHost?.notifyOutsidePress(vPx.toInt(), vPy.toInt()) popupHost?.notifyOutsidePress(vPx.toInt(), vPy.toInt())
} }
host.onPointerRaw(vPx, vPy, vType, vBtn, SDL_GetTicks().toLong()) host.onPointerRaw(vPx, vPy, vType, vBtn, SDL_GetTicks().toLong())
@@ -778,6 +347,7 @@ internal class WindowInstance(
synthetic hover re-dispatch and fire one Exit at the last position so a synthetic hover re-dispatch and fire one Exit at the last position so a
hovered widget clears its highlight instead of sticking forever. */ hovered widget clears its highlight instead of sticking forever. */
fun onPointerExit() { fun onPointerExit() {
if (!facade.isEnabled) return
needsFrame = true needsFrame = true
if (hasMousePos) { if (hasMousePos) {
host.onPointerRaw(lastMouseX, lastMouseY, 3, 0, SDL_GetTicks().toLong()) host.onPointerRaw(lastMouseX, lastMouseY, 3, 0, SDL_GetTicks().toLong())
@@ -786,6 +356,7 @@ internal class WindowInstance(
} }
fun onWheelEvent(inEvent: AppEvent.MouseWheel) { fun onWheelEvent(inEvent: AppEvent.MouseWheel) {
if (!facade.isEnabled) return
needsFrame = true needsFrame = true
installGlobals() installGlobals()
val vDpr = backend.pixelDensity val vDpr = backend.pixelDensity
@@ -793,20 +364,36 @@ internal class WindowInstance(
} }
fun onKeyEvent(inEvent: AppEvent.Key) { fun onKeyEvent(inEvent: AppEvent.Key) {
if (!facade.isEnabled) return
needsFrame = true needsFrame = true
installGlobals() installGlobals()
// Focused chain → window-level shortcuts → back navigation (Escape). // Compose Desktop's order: window preview → focused content → window
if (!host.dispatchKeyEvent(inEvent.event) && !facade.dispatchKeyShortcut(inEvent.event)) { // onKeyEvent → app shortcut handler → back navigation (Escape).
if (previewKeyHandler().invoke(inEvent.event)) return
if (host.dispatchKeyEvent(inEvent.event)) return
if (keyHandler().invoke(inEvent.event)) return
if (!facade.dispatchKeyShortcut(inEvent.event)) {
backNavigationInput.onKeyEvent(inEvent.event) backNavigationInput.onKeyEvent(inEvent.event)
} }
} }
/** Applies the mutable window attributes. Called at init from the creation
flags and again whenever Window() sees one of them change. */
fun applyAttributes(inAttrs: WindowAttributes) {
facade.setUndecorated(inAttrs.undecorated)
facade.setResizable(inAttrs.resizable)
facade.setAlwaysOnTop(inAttrs.alwaysOnTop)
facade.setFocusable(inAttrs.focusable)
facade.setEnabled(inAttrs.enabled)
}
fun onTextInputEvent(inEvent: AppEvent.TextInput) { fun onTextInputEvent(inEvent: AppEvent.TextInput) {
if (!facade.isEnabled) return
needsFrame = true needsFrame = true
installGlobals() installGlobals()
// A focused text field runs an IME session (ComposeOwner.textInputSession): // A focused text field runs an IME session (ComposeOwner.textInputSession):
// commit through it so the text REPLACES any active composition. Falls back // commit through it so the text REPLACES any active composition. Falls back
// to synthesising typed KeyEvents when no field is focused — SDL key events // to synthesising typed KeyEvents when no field is focused - SDL key events
// carry UNSHIFTED keycodes (no uppercase/numpad/dead keys), so committed // carry UNSHIFTED keycodes (no uppercase/numpad/dead keys), so committed
// text is the only layout-correct character source. // text is the only layout-correct character source.
if (!com.compose.sdl.text.input.ImeBridge.commit(inEvent.text)) { if (!com.compose.sdl.text.input.ImeBridge.commit(inEvent.text)) {
@@ -815,6 +402,7 @@ internal class WindowInstance(
} }
fun onTextEditingEvent(inEvent: AppEvent.TextEditing) { fun onTextEditingEvent(inEvent: AppEvent.TextEditing) {
if (!facade.isEnabled) return
needsFrame = true needsFrame = true
installGlobals() installGlobals()
// IME preedit: show the composing (underlined) region in the focused field. // IME preedit: show the composing (underlined) region in the focused field.
@@ -844,6 +432,7 @@ internal class WindowInstance(
} }
fun onDropEvent(inEvent: AppEvent.Drop) { fun onDropEvent(inEvent: AppEvent.Drop) {
if (!facade.isEnabled) return
needsFrame = true needsFrame = true
installGlobals() installGlobals()
// SDL fires drop coords in logical points at DPR-1; scale to the pixel // SDL fires drop coords in logical points at DPR-1; scale to the pixel
@@ -865,7 +454,7 @@ internal class WindowInstance(
facade.onResized() facade.onResized()
} }
/** OS light/dark theme changed — re-pick the theme-appropriate window icon /** OS light/dark theme changed - re-pick the theme-appropriate window icon
(no-op if this window has no icon configured or the choice is unchanged). */ (no-op if this window has no icon configured or the choice is unchanged). */
fun onSystemThemeChanged() { fun onSystemThemeChanged() {
backend.applyThemeIcon() backend.applyThemeIcon()
@@ -889,7 +478,7 @@ internal class WindowInstance(
/** True while this window still has pending work: a state/layout/draw invalidation /** True while this window still has pending work: a state/layout/draw invalidation
(needsFrame), recomposer work, or a composition/node animation awaiting the next (needsFrame), recomposer work, or a composition/node animation awaiting the next
frame. Sampled by windowHasInvalidations() from onFrame probes — the quiescence frame. Sampled by windowHasInvalidations() from onFrame probes - the quiescence
signal for render-to-settle screenshot capture. */ signal for render-to-settle screenshot capture. */
fun hasInvalidations(): Boolean = fun hasInvalidations(): Boolean =
needsFrame || recomposer?.hasPendingWork == true || needsFrame || recomposer?.hasPendingWork == true ||
@@ -905,13 +494,13 @@ internal class WindowInstance(
// Charge the GPU back-buffer acquire to its own phase. On Metal this is // Charge the GPU back-buffer acquire to its own phase. On Metal this is
// where nextDrawable() blocks on vsync (the natural frame pacing), so // where nextDrawable() blocks on vsync (the natural frame pacing), so
// folding it into "layout" made the profiler read the vsync wait as // folding it into "layout" made the profiler read the vsync wait as
// layout cost — a ~6-7ms phantom that hid the real (tiny) layout time. // layout cost - a ~6-7ms phantom that hid the real (tiny) layout time.
FrameProfiler.phase(" acquire") FrameProfiler.phase(" acquire")
host.setConstraints(backend.pixelWidth, backend.pixelHeight) host.setConstraints(backend.pixelWidth, backend.pixelHeight)
// Deliver any state written by this iteration's frame-clock continuations // Deliver any state written by this iteration's frame-clock continuations
// (withFrameNanos animations — notably smooth wheel scrolling) BEFORE we lay // (withFrameNanos animations - notably smooth wheel scrolling) BEFORE we lay
// out. Those writes happen in the per-window pump's sendFrame()/yield(), which // out. Those writes happen in the per-window pump's sendFrame()/yield(), which
// is AFTER the loop's last sendApplyNotifications() — so without this the // is AFTER the loop's last sendApplyNotifications() - so without this the
// relayout they trigger wasn't registered until the next frame, and the frame // relayout they trigger wasn't registered until the next frame, and the frame
// drew last frame's positions. That one-frame trail read as "the clip is a // drew last frame's positions. That one-frame trail read as "the clip is a
// frame late" when scrolling (content briefly drawn past the viewport edge). // frame late" when scrolling (content briefly drawn past the viewport edge).
@@ -938,14 +527,14 @@ internal class WindowInstance(
FrameProfiler.phase(" present") FrameProfiler.phase(" present")
frameIndex++ frameIndex++
// FPS — instantaneous inter-frame rate, EMA-smoothed, title refreshed // FPS - instantaneous inter-frame rate, EMA-smoothed, title refreshed
// ~4x/sec. This shows within ~2 rendered frames of ANY activity rather // ~4x/sec. This shows within ~2 rendered frames of ANY activity rather
// than waiting for a full second of unbroken rendering to accumulate // than waiting for a full second of unbroken rendering to accumulate
// (the old fixed-window counter never got its first update: bursty // (the old fixed-window counter never got its first update: bursty
// interaction kept idling before 1s elapsed, and the idle-skip reset the // interaction kept idling before 1s elapsed, and the idle-skip reset the
// window each time — so FPS only appeared during a long enough page // window each time - so FPS only appeared during a long enough page
// transition). A dt outside 1..100ms is the first frame or a resume from // transition). A dt outside 1..100ms is the first frame or a resume from
// idle (the gap isn't a real frame interval), so it's not sampled — the // idle (the gap isn't a real frame interval), so it's not sampled - the
// title just holds the last active rate while idle. // title just holds the last active rate while idle.
val vNowMs = SDL_GetTicks() val vNowMs = SDL_GetTicks()
val vDt = (vNowMs - fpsLastFrameMs).toInt() val vDt = (vNowMs - fpsLastFrameMs).toInt()
@@ -984,6 +573,3 @@ internal class WindowInstance(
backend.destroy(inQuitSdl = false) backend.destroy(inQuitSdl = false)
} }
} }
// WindowArchitectureOwner -> WindowArchitectureOwner.kt
// BackNavigationInput / dispatchTypedText -> WindowInputHelpers.kt
@@ -1,9 +1,9 @@
// :foundation-layout — androidx.compose.foundation.layout.*, vendored VERBATIM // :foundation-layout - androidx.compose.foundation.layout.*, vendored VERBATIM
// from upstream into src/vendor/. Split out of :foundation to mirror Compose // from upstream into src/vendor/. Split out of :foundation to mirror Compose
// Multiplatform's module packaging. // Multiplatform's module packaging.
// //
// Provenance = foundation-layout/compose-fork.txt + scripts/compose-fork/compose.properties. // Provenance = foundation-layout/compose-fork.txt + scripts/compose-fork/compose.properties.
// Never hand-edit files under src/vendor/ — change the manifest and re-run // Never hand-edit files under src/vendor/ - change the manifest and re-run
// `python scripts/compose-fork/sync.py compose/foundation/foundation-layout`. // `python scripts/compose-fork/sync.py compose/foundation/foundation-layout`.
// //
// Publication artifactId: desktop-foundation-layout. // Publication artifactId: desktop-foundation-layout.
@@ -27,7 +27,7 @@ kotlin {
commonMain { commonMain {
dependencies { dependencies {
// foundation.layout.* only needs ui (+ runtime/collection transitively). // foundation.layout.* only needs ui (+ runtime/collection transitively).
api(project(":ui")) api(project(":compose:ui:ui"))
} }
kotlin.srcDir("src/vendor/common/kotlin") kotlin.srcDir("src/vendor/common/kotlin")
} }
@@ -778,6 +778,17 @@ abstract interface androidx.compose.foundation.text/TextAutoSize { // androidx.c
} }
} }
abstract interface androidx.compose.foundation.v2/ScrollbarAdapter { // androidx.compose.foundation.v2/ScrollbarAdapter|null[0]
abstract val contentSize // androidx.compose.foundation.v2/ScrollbarAdapter.contentSize|{}contentSize[0]
abstract fun <get-contentSize>(): kotlin/Double // androidx.compose.foundation.v2/ScrollbarAdapter.contentSize.<get-contentSize>|<get-contentSize>(){}[0]
abstract val scrollOffset // androidx.compose.foundation.v2/ScrollbarAdapter.scrollOffset|{}scrollOffset[0]
abstract fun <get-scrollOffset>(): kotlin/Double // androidx.compose.foundation.v2/ScrollbarAdapter.scrollOffset.<get-scrollOffset>|<get-scrollOffset>(){}[0]
abstract val viewportSize // androidx.compose.foundation.v2/ScrollbarAdapter.viewportSize|{}viewportSize[0]
abstract fun <get-viewportSize>(): kotlin/Double // androidx.compose.foundation.v2/ScrollbarAdapter.viewportSize.<get-viewportSize>|<get-viewportSize>(){}[0]
abstract suspend fun scrollTo(kotlin/Double) // androidx.compose.foundation.v2/ScrollbarAdapter.scrollTo|scrollTo(kotlin.Double){}[0]
}
abstract interface androidx.compose.foundation/BasicTooltipState { // androidx.compose.foundation/BasicTooltipState|null[0] abstract interface androidx.compose.foundation/BasicTooltipState { // androidx.compose.foundation/BasicTooltipState|null[0]
abstract val isPersistent // androidx.compose.foundation/BasicTooltipState.isPersistent|{}isPersistent[0] abstract val isPersistent // androidx.compose.foundation/BasicTooltipState.isPersistent|{}isPersistent[0]
abstract fun <get-isPersistent>(): kotlin/Boolean // androidx.compose.foundation/BasicTooltipState.isPersistent.<get-isPersistent>|<get-isPersistent>(){}[0] abstract fun <get-isPersistent>(): kotlin/Boolean // androidx.compose.foundation/BasicTooltipState.isPersistent.<get-isPersistent>|<get-isPersistent>(){}[0]
@@ -830,15 +841,12 @@ abstract interface androidx.compose.foundation/ScrollIndicatorState { // android
abstract fun <get-viewportSize>(): kotlin/Int // androidx.compose.foundation/ScrollIndicatorState.viewportSize.<get-viewportSize>|<get-viewportSize>(){}[0] abstract fun <get-viewportSize>(): kotlin/Int // androidx.compose.foundation/ScrollIndicatorState.viewportSize.<get-viewportSize>|<get-viewportSize>(){}[0]
} }
abstract interface com.compose.sdl.scrollbar/ScrollbarAdapter { // com.compose.sdl.scrollbar/ScrollbarAdapter|null[0] abstract interface androidx.compose.foundation/ScrollbarAdapter { // androidx.compose.foundation/ScrollbarAdapter|null[0]
abstract val contentSize // com.compose.sdl.scrollbar/ScrollbarAdapter.contentSize|{}contentSize[0] abstract val scrollOffset // androidx.compose.foundation/ScrollbarAdapter.scrollOffset|{}scrollOffset[0]
abstract fun <get-contentSize>(): kotlin/Double // com.compose.sdl.scrollbar/ScrollbarAdapter.contentSize.<get-contentSize>|<get-contentSize>(){}[0] abstract fun <get-scrollOffset>(): kotlin/Float // androidx.compose.foundation/ScrollbarAdapter.scrollOffset.<get-scrollOffset>|<get-scrollOffset>(){}[0]
abstract val scrollOffset // com.compose.sdl.scrollbar/ScrollbarAdapter.scrollOffset|{}scrollOffset[0]
abstract fun <get-scrollOffset>(): kotlin/Double // com.compose.sdl.scrollbar/ScrollbarAdapter.scrollOffset.<get-scrollOffset>|<get-scrollOffset>(){}[0]
abstract val viewportSize // com.compose.sdl.scrollbar/ScrollbarAdapter.viewportSize|{}viewportSize[0]
abstract fun <get-viewportSize>(): kotlin/Double // com.compose.sdl.scrollbar/ScrollbarAdapter.viewportSize.<get-viewportSize>|<get-viewportSize>(){}[0]
abstract suspend fun scrollTo(kotlin/Double) // com.compose.sdl.scrollbar/ScrollbarAdapter.scrollTo|scrollTo(kotlin.Double){}[0] abstract fun maxScrollOffset(kotlin/Int): kotlin/Float // androidx.compose.foundation/ScrollbarAdapter.maxScrollOffset|maxScrollOffset(kotlin.Int){}[0]
abstract suspend fun scrollTo(kotlin/Int, kotlin/Float) // androidx.compose.foundation/ScrollbarAdapter.scrollTo|scrollTo(kotlin.Int;kotlin.Float){}[0]
} }
sealed interface <#A: out kotlin/Any?> androidx.compose.foundation.lazy.layout/IntervalList { // androidx.compose.foundation.lazy.layout/IntervalList|null[0] sealed interface <#A: out kotlin/Any?> androidx.compose.foundation.lazy.layout/IntervalList { // androidx.compose.foundation.lazy.layout/IntervalList|null[0]
@@ -1829,21 +1837,32 @@ final class androidx.compose.foundation/ScrollState : androidx.compose.foundatio
} }
} }
final class com.compose.sdl.scrollbar/ScrollbarStyle { // com.compose.sdl.scrollbar/ScrollbarStyle|null[0] final class androidx.compose.foundation/ScrollbarStyle { // androidx.compose.foundation/ScrollbarStyle|null[0]
constructor <init>(androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Shape, kotlin/Int, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color) // com.compose.sdl.scrollbar/ScrollbarStyle.<init>|<init>(androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Shape;kotlin.Int;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0] constructor <init>(androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Shape, kotlin/Int, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color) // androidx.compose.foundation/ScrollbarStyle.<init>|<init>(androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Shape;kotlin.Int;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0]
final val hoverColor // com.compose.sdl.scrollbar/ScrollbarStyle.hoverColor|{}hoverColor[0] final val hoverColor // androidx.compose.foundation/ScrollbarStyle.hoverColor|{}hoverColor[0]
final fun <get-hoverColor>(): androidx.compose.ui.graphics/Color // com.compose.sdl.scrollbar/ScrollbarStyle.hoverColor.<get-hoverColor>|<get-hoverColor>(){}[0] final fun <get-hoverColor>(): androidx.compose.ui.graphics/Color // androidx.compose.foundation/ScrollbarStyle.hoverColor.<get-hoverColor>|<get-hoverColor>(){}[0]
final val hoverDurationMillis // com.compose.sdl.scrollbar/ScrollbarStyle.hoverDurationMillis|{}hoverDurationMillis[0] final val hoverDurationMillis // androidx.compose.foundation/ScrollbarStyle.hoverDurationMillis|{}hoverDurationMillis[0]
final fun <get-hoverDurationMillis>(): kotlin/Int // com.compose.sdl.scrollbar/ScrollbarStyle.hoverDurationMillis.<get-hoverDurationMillis>|<get-hoverDurationMillis>(){}[0] final fun <get-hoverDurationMillis>(): kotlin/Int // androidx.compose.foundation/ScrollbarStyle.hoverDurationMillis.<get-hoverDurationMillis>|<get-hoverDurationMillis>(){}[0]
final val minimalHeight // com.compose.sdl.scrollbar/ScrollbarStyle.minimalHeight|{}minimalHeight[0] final val minimalHeight // androidx.compose.foundation/ScrollbarStyle.minimalHeight|{}minimalHeight[0]
final fun <get-minimalHeight>(): androidx.compose.ui.unit/Dp // com.compose.sdl.scrollbar/ScrollbarStyle.minimalHeight.<get-minimalHeight>|<get-minimalHeight>(){}[0] final fun <get-minimalHeight>(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/ScrollbarStyle.minimalHeight.<get-minimalHeight>|<get-minimalHeight>(){}[0]
final val shape // com.compose.sdl.scrollbar/ScrollbarStyle.shape|{}shape[0] final val shape // androidx.compose.foundation/ScrollbarStyle.shape|{}shape[0]
final fun <get-shape>(): androidx.compose.ui.graphics/Shape // com.compose.sdl.scrollbar/ScrollbarStyle.shape.<get-shape>|<get-shape>(){}[0] final fun <get-shape>(): androidx.compose.ui.graphics/Shape // androidx.compose.foundation/ScrollbarStyle.shape.<get-shape>|<get-shape>(){}[0]
final val thickness // com.compose.sdl.scrollbar/ScrollbarStyle.thickness|{}thickness[0] final val thickness // androidx.compose.foundation/ScrollbarStyle.thickness|{}thickness[0]
final fun <get-thickness>(): androidx.compose.ui.unit/Dp // com.compose.sdl.scrollbar/ScrollbarStyle.thickness.<get-thickness>|<get-thickness>(){}[0] final fun <get-thickness>(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/ScrollbarStyle.thickness.<get-thickness>|<get-thickness>(){}[0]
final val unhoverColor // com.compose.sdl.scrollbar/ScrollbarStyle.unhoverColor|{}unhoverColor[0] final val unhoverColor // androidx.compose.foundation/ScrollbarStyle.unhoverColor|{}unhoverColor[0]
final fun <get-unhoverColor>(): androidx.compose.ui.graphics/Color // com.compose.sdl.scrollbar/ScrollbarStyle.unhoverColor.<get-unhoverColor>|<get-unhoverColor>(){}[0] final fun <get-unhoverColor>(): androidx.compose.ui.graphics/Color // androidx.compose.foundation/ScrollbarStyle.unhoverColor.<get-unhoverColor>|<get-unhoverColor>(){}[0]
final fun component1(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/ScrollbarStyle.component1|component1(){}[0]
final fun component2(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/ScrollbarStyle.component2|component2(){}[0]
final fun component3(): androidx.compose.ui.graphics/Shape // androidx.compose.foundation/ScrollbarStyle.component3|component3(){}[0]
final fun component4(): kotlin/Int // androidx.compose.foundation/ScrollbarStyle.component4|component4(){}[0]
final fun component5(): androidx.compose.ui.graphics/Color // androidx.compose.foundation/ScrollbarStyle.component5|component5(){}[0]
final fun component6(): androidx.compose.ui.graphics/Color // androidx.compose.foundation/ScrollbarStyle.component6|component6(){}[0]
final fun copy(androidx.compose.ui.unit/Dp = ..., androidx.compose.ui.unit/Dp = ..., androidx.compose.ui.graphics/Shape = ..., kotlin/Int = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ...): androidx.compose.foundation/ScrollbarStyle // androidx.compose.foundation/ScrollbarStyle.copy|copy(androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Shape;kotlin.Int;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // androidx.compose.foundation/ScrollbarStyle.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // androidx.compose.foundation/ScrollbarStyle.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // androidx.compose.foundation/ScrollbarStyle.toString|toString(){}[0]
} }
final value class androidx.compose.foundation.lazy.grid/GridItemSpan { // androidx.compose.foundation.lazy.grid/GridItemSpan|null[0] final value class androidx.compose.foundation.lazy.grid/GridItemSpan { // androidx.compose.foundation.lazy.grid/GridItemSpan|null[0]
@@ -2071,13 +2090,6 @@ final object androidx.compose.foundation/MarqueeDefaults { // androidx.compose.f
final fun <get-Velocity>(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/MarqueeDefaults.Velocity.<get-Velocity>|<get-Velocity>(){}[0] final fun <get-Velocity>(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/MarqueeDefaults.Velocity.<get-Velocity>|<get-Velocity>(){}[0]
} }
final object com.compose.sdl.icons/IconDefaults { // com.compose.sdl.icons/IconDefaults|null[0]
final val DefaultIconSize // com.compose.sdl.icons/IconDefaults.DefaultIconSize|{}DefaultIconSize[0]
final fun <get-DefaultIconSize>(): androidx.compose.ui.unit/Dp // com.compose.sdl.icons/IconDefaults.DefaultIconSize.<get-DefaultIconSize>|<get-DefaultIconSize>(){}[0]
final val LocalContentColor // com.compose.sdl.icons/IconDefaults.LocalContentColor|{}LocalContentColor[0]
final fun <get-LocalContentColor>(): androidx.compose.ui.graphics/Color // com.compose.sdl.icons/IconDefaults.LocalContentColor.<get-LocalContentColor>|<get-LocalContentColor>(){}[0]
}
final object com.compose.sdl.widgets/SplitPaneDefaults { // com.compose.sdl.widgets/SplitPaneDefaults|null[0] final object com.compose.sdl.widgets/SplitPaneDefaults { // com.compose.sdl.widgets/SplitPaneDefaults|null[0]
final val DividerThickness // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness|{}DividerThickness[0] final val DividerThickness // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness|{}DividerThickness[0]
final fun <get-DividerThickness>(): androidx.compose.ui.unit/Dp // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness.<get-DividerThickness>|<get-DividerThickness>(){}[0] final fun <get-DividerThickness>(): androidx.compose.ui.unit/Dp // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness.<get-DividerThickness>|<get-DividerThickness>(){}[0]
@@ -2197,10 +2209,15 @@ final val androidx.compose.foundation.text/androidx_compose_foundation_text_Keyb
final val androidx.compose.foundation.text/androidx_compose_foundation_text_KeyboardOptions$stableprop // androidx.compose.foundation.text/androidx_compose_foundation_text_KeyboardOptions$stableprop|#static{}androidx_compose_foundation_text_KeyboardOptions$stableprop[0] final val androidx.compose.foundation.text/androidx_compose_foundation_text_KeyboardOptions$stableprop // androidx.compose.foundation.text/androidx_compose_foundation_text_KeyboardOptions$stableprop|#static{}androidx_compose_foundation_text_KeyboardOptions$stableprop[0]
final val androidx.compose.foundation.text/androidx_compose_foundation_text_TextAutoSizeDefaults$stableprop // androidx.compose.foundation.text/androidx_compose_foundation_text_TextAutoSizeDefaults$stableprop|#static{}androidx_compose_foundation_text_TextAutoSizeDefaults$stableprop[0] final val androidx.compose.foundation.text/androidx_compose_foundation_text_TextAutoSizeDefaults$stableprop // androidx.compose.foundation.text/androidx_compose_foundation_text_TextAutoSizeDefaults$stableprop|#static{}androidx_compose_foundation_text_TextAutoSizeDefaults$stableprop[0]
final val androidx.compose.foundation.text/androidx_compose_foundation_text_TextFieldScrollState$stableprop // androidx.compose.foundation.text/androidx_compose_foundation_text_TextFieldScrollState$stableprop|#static{}androidx_compose_foundation_text_TextFieldScrollState$stableprop[0] final val androidx.compose.foundation.text/androidx_compose_foundation_text_TextFieldScrollState$stableprop // androidx.compose.foundation.text/androidx_compose_foundation_text_TextFieldScrollState$stableprop|#static{}androidx_compose_foundation_text_TextFieldScrollState$stableprop[0]
final val androidx.compose.foundation.v2/androidx_compose_foundation_v2_LazyLineContentAdapter_VisibleLine$stableprop // androidx.compose.foundation.v2/androidx_compose_foundation_v2_LazyLineContentAdapter_VisibleLine$stableprop|#static{}androidx_compose_foundation_v2_LazyLineContentAdapter_VisibleLine$stableprop[0]
final val androidx.compose.foundation.v2/maxScrollOffset // androidx.compose.foundation.v2/maxScrollOffset|@androidx.compose.foundation.v2.ScrollbarAdapter{}maxScrollOffset[0]
final fun (androidx.compose.foundation.v2/ScrollbarAdapter).<get-maxScrollOffset>(): kotlin/Double // androidx.compose.foundation.v2/maxScrollOffset.<get-maxScrollOffset>|<get-maxScrollOffset>@androidx.compose.foundation.v2.ScrollbarAdapter(){}[0]
final val androidx.compose.foundation/LocalIndication // androidx.compose.foundation/LocalIndication|{}LocalIndication[0] final val androidx.compose.foundation/LocalIndication // androidx.compose.foundation/LocalIndication|{}LocalIndication[0]
final fun <get-LocalIndication>(): androidx.compose.runtime/ProvidableCompositionLocal<androidx.compose.foundation/Indication> // androidx.compose.foundation/LocalIndication.<get-LocalIndication>|<get-LocalIndication>(){}[0] final fun <get-LocalIndication>(): androidx.compose.runtime/ProvidableCompositionLocal<androidx.compose.foundation/Indication> // androidx.compose.foundation/LocalIndication.<get-LocalIndication>|<get-LocalIndication>(){}[0]
final val androidx.compose.foundation/LocalOverscrollFactory // androidx.compose.foundation/LocalOverscrollFactory|{}LocalOverscrollFactory[0] final val androidx.compose.foundation/LocalOverscrollFactory // androidx.compose.foundation/LocalOverscrollFactory|{}LocalOverscrollFactory[0]
final fun <get-LocalOverscrollFactory>(): androidx.compose.runtime/ProvidableCompositionLocal<androidx.compose.foundation/OverscrollFactory?> // androidx.compose.foundation/LocalOverscrollFactory.<get-LocalOverscrollFactory>|<get-LocalOverscrollFactory>(){}[0] final fun <get-LocalOverscrollFactory>(): androidx.compose.runtime/ProvidableCompositionLocal<androidx.compose.foundation/OverscrollFactory?> // androidx.compose.foundation/LocalOverscrollFactory.<get-LocalOverscrollFactory>|<get-LocalOverscrollFactory>(){}[0]
final val androidx.compose.foundation/LocalScrollbarStyle // androidx.compose.foundation/LocalScrollbarStyle|{}LocalScrollbarStyle[0]
final fun <get-LocalScrollbarStyle>(): androidx.compose.runtime/ProvidableCompositionLocal<androidx.compose.foundation/ScrollbarStyle> // androidx.compose.foundation/LocalScrollbarStyle.<get-LocalScrollbarStyle>|<get-LocalScrollbarStyle>(){}[0]
final val androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop // androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop|#static{}androidx_compose_foundation_BasicTooltipDefaults$stableprop[0] final val androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop // androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop|#static{}androidx_compose_foundation_BasicTooltipDefaults$stableprop[0]
final val androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop // androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop|#static{}androidx_compose_foundation_BorderStroke$stableprop[0] final val androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop // androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop|#static{}androidx_compose_foundation_BorderStroke$stableprop[0]
final val androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop // androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop|#static{}androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop[0] final val androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop // androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop|#static{}androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop[0]
@@ -2208,10 +2225,7 @@ final val androidx.compose.foundation/androidx_compose_foundation_ComposeFoundat
final val androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop // androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop|#static{}androidx_compose_foundation_MarqueeDefaults$stableprop[0] final val androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop // androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop|#static{}androidx_compose_foundation_MarqueeDefaults$stableprop[0]
final val androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop // androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop|#static{}androidx_compose_foundation_MutatorMutex$stableprop[0] final val androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop // androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop|#static{}androidx_compose_foundation_MutatorMutex$stableprop[0]
final val androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop // androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop|#static{}androidx_compose_foundation_ScrollState$stableprop[0] final val androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop // androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop|#static{}androidx_compose_foundation_ScrollState$stableprop[0]
final val com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop // com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop|#static{}com_compose_sdl_icons_IconDefaults$stableprop[0] final val androidx.compose.foundation/androidx_compose_foundation_ScrollbarStyle$stableprop // androidx.compose.foundation/androidx_compose_foundation_ScrollbarStyle$stableprop|#static{}androidx_compose_foundation_ScrollbarStyle$stableprop[0]
final val com.compose.sdl.scrollbar/LocalScrollbarStyle // com.compose.sdl.scrollbar/LocalScrollbarStyle|{}LocalScrollbarStyle[0]
final fun <get-LocalScrollbarStyle>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl.scrollbar/ScrollbarStyle> // com.compose.sdl.scrollbar/LocalScrollbarStyle.<get-LocalScrollbarStyle>|<get-LocalScrollbarStyle>(){}[0]
final val com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop // com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop|#static{}com_compose_sdl_scrollbar_ScrollbarStyle$stableprop[0]
final val com.compose.sdl.widgets/com_compose_sdl_widgets_SplitPaneDefaults$stableprop // com.compose.sdl.widgets/com_compose_sdl_widgets_SplitPaneDefaults$stableprop|#static{}com_compose_sdl_widgets_SplitPaneDefaults$stableprop[0] final val com.compose.sdl.widgets/com_compose_sdl_widgets_SplitPaneDefaults$stableprop // com.compose.sdl.widgets/com_compose_sdl_widgets_SplitPaneDefaults$stableprop|#static{}com_compose_sdl_widgets_SplitPaneDefaults$stableprop[0]
final fun (androidx.compose.foundation.content/TransferableContent).androidx.compose.foundation.content/hasMediaType(androidx.compose.foundation.content/MediaType): kotlin/Boolean // androidx.compose.foundation.content/hasMediaType|hasMediaType@androidx.compose.foundation.content.TransferableContent(androidx.compose.foundation.content.MediaType){}[0] final fun (androidx.compose.foundation.content/TransferableContent).androidx.compose.foundation.content/hasMediaType(androidx.compose.foundation.content/MediaType): kotlin/Boolean // androidx.compose.foundation.content/hasMediaType|hasMediaType@androidx.compose.foundation.content.TransferableContent(androidx.compose.foundation.content.MediaType){}[0]
@@ -2585,18 +2599,29 @@ final fun androidx.compose.foundation.text/androidx_compose_foundation_text_Text
final fun androidx.compose.foundation.text/rememberTextFieldHorizontalScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation.text/TextFieldScrollState // androidx.compose.foundation.text/rememberTextFieldHorizontalScrollState|rememberTextFieldHorizontalScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation.text/rememberTextFieldHorizontalScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation.text/TextFieldScrollState // androidx.compose.foundation.text/rememberTextFieldHorizontalScrollState|rememberTextFieldHorizontalScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation.text/rememberTextFieldScrollState(androidx.compose.foundation.gestures/Orientation, kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation.text/TextFieldScrollState // androidx.compose.foundation.text/rememberTextFieldScrollState|rememberTextFieldScrollState(androidx.compose.foundation.gestures.Orientation;kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation.text/rememberTextFieldScrollState(androidx.compose.foundation.gestures/Orientation, kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation.text/TextFieldScrollState // androidx.compose.foundation.text/rememberTextFieldScrollState|rememberTextFieldScrollState(androidx.compose.foundation.gestures.Orientation;kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation.text/rememberTextFieldVerticalScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation.text/TextFieldScrollState // androidx.compose.foundation.text/rememberTextFieldVerticalScrollState|rememberTextFieldVerticalScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation.text/rememberTextFieldVerticalScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation.text/TextFieldScrollState // androidx.compose.foundation.text/rememberTextFieldVerticalScrollState|rememberTextFieldVerticalScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation.v2/androidx_compose_foundation_v2_LazyLineContentAdapter_VisibleLine$stableprop_getter(): kotlin/Int // androidx.compose.foundation.v2/androidx_compose_foundation_v2_LazyLineContentAdapter_VisibleLine$stableprop_getter|androidx_compose_foundation_v2_LazyLineContentAdapter_VisibleLine$stableprop_getter(){}[0]
final fun androidx.compose.foundation/BasicTooltipBox(androidx.compose.ui.window/PopupPositionProvider, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.foundation/BasicTooltipState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/BasicTooltipBox|BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.foundation.BasicTooltipState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation/BasicTooltipBox(androidx.compose.ui.window/PopupPositionProvider, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.foundation/BasicTooltipState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/BasicTooltipBox|BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.foundation.BasicTooltipState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/BasicTooltipBox(androidx.compose.ui.window/PopupPositionProvider, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.foundation/BasicTooltipState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/BasicTooltipBox|BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.foundation.BasicTooltipState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation/BasicTooltipBox(androidx.compose.ui.window/PopupPositionProvider, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.foundation/BasicTooltipState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/BasicTooltipBox|BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.foundation.BasicTooltipState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/BasicTooltipState(kotlin/Boolean = ..., kotlin/Boolean = ..., androidx.compose.foundation/MutatorMutex = ...): androidx.compose.foundation/BasicTooltipState // androidx.compose.foundation/BasicTooltipState|BasicTooltipState(kotlin.Boolean;kotlin.Boolean;androidx.compose.foundation.MutatorMutex){}[0] final fun androidx.compose.foundation/BasicTooltipState(kotlin/Boolean = ..., kotlin/Boolean = ..., androidx.compose.foundation/MutatorMutex = ...): androidx.compose.foundation/BasicTooltipState // androidx.compose.foundation/BasicTooltipState|BasicTooltipState(kotlin.Boolean;kotlin.Boolean;androidx.compose.foundation.MutatorMutex){}[0]
final fun androidx.compose.foundation/BorderStroke(androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color): androidx.compose.foundation/BorderStroke // androidx.compose.foundation/BorderStroke|BorderStroke(androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color){}[0] final fun androidx.compose.foundation/BorderStroke(androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color): androidx.compose.foundation/BorderStroke // androidx.compose.foundation/BorderStroke|BorderStroke(androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color){}[0]
final fun androidx.compose.foundation/Canvas(androidx.compose.ui/Modifier, kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // androidx.compose.foundation/Canvas|Canvas(androidx.compose.ui.Modifier;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0] final fun androidx.compose.foundation/Canvas(androidx.compose.ui/Modifier, kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // androidx.compose.foundation/Canvas|Canvas(androidx.compose.ui.Modifier;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun androidx.compose.foundation/Canvas(androidx.compose.ui/Modifier, kotlin/String, kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // androidx.compose.foundation/Canvas|Canvas(androidx.compose.ui.Modifier;kotlin.String;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0] final fun androidx.compose.foundation/Canvas(androidx.compose.ui/Modifier, kotlin/String, kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // androidx.compose.foundation/Canvas|Canvas(androidx.compose.ui.Modifier;kotlin.String;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun androidx.compose.foundation/HorizontalScrollbar(androidx.compose.foundation.v2/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.foundation/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/HorizontalScrollbar|HorizontalScrollbar(androidx.compose.foundation.v2.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.foundation.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/HorizontalScrollbar(androidx.compose.foundation/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.foundation/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/HorizontalScrollbar|HorizontalScrollbar(androidx.compose.foundation.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.foundation.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics.painter/Painter, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.painter.Painter;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics.painter/Painter, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.painter.Painter;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics.vector/ImageVector, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.vector.ImageVector;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics.vector/ImageVector, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.vector.ImageVector;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics/ImageBitmap, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.ImageBitmap;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics/ImageBitmap, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.ImageBitmap;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics/ImageBitmap, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.ImageBitmap;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics/ImageBitmap, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.ImageBitmap;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics/ImageBitmap, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.ImageBitmap;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics/ImageBitmap, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.ImageBitmap;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/MarqueeSpacing(androidx.compose.ui.unit/Dp): androidx.compose.foundation/MarqueeSpacing // androidx.compose.foundation/MarqueeSpacing|MarqueeSpacing(androidx.compose.ui.unit.Dp){}[0] final fun androidx.compose.foundation/MarqueeSpacing(androidx.compose.ui.unit/Dp): androidx.compose.foundation/MarqueeSpacing // androidx.compose.foundation/MarqueeSpacing|MarqueeSpacing(androidx.compose.ui.unit.Dp){}[0]
final fun androidx.compose.foundation/OldScrollbarAdapter(androidx.compose.foundation.lazy/LazyListState): androidx.compose.foundation/ScrollbarAdapter // androidx.compose.foundation/OldScrollbarAdapter|OldScrollbarAdapter(androidx.compose.foundation.lazy.LazyListState){}[0]
final fun androidx.compose.foundation/OldScrollbarAdapter(androidx.compose.foundation/ScrollState): androidx.compose.foundation/ScrollbarAdapter // androidx.compose.foundation/OldScrollbarAdapter|OldScrollbarAdapter(androidx.compose.foundation.ScrollState){}[0]
final fun androidx.compose.foundation/ScrollbarAdapter(androidx.compose.foundation.lazy.grid/LazyGridState): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/ScrollbarAdapter|ScrollbarAdapter(androidx.compose.foundation.lazy.grid.LazyGridState){}[0]
final fun androidx.compose.foundation/ScrollbarAdapter(androidx.compose.foundation.lazy/LazyListState): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/ScrollbarAdapter|ScrollbarAdapter(androidx.compose.foundation.lazy.LazyListState){}[0]
final fun androidx.compose.foundation/ScrollbarAdapter(androidx.compose.foundation.text/TextFieldScrollState): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/ScrollbarAdapter|ScrollbarAdapter(androidx.compose.foundation.text.TextFieldScrollState){}[0]
final fun androidx.compose.foundation/ScrollbarAdapter(androidx.compose.foundation/ScrollState): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/ScrollbarAdapter|ScrollbarAdapter(androidx.compose.foundation.ScrollState){}[0]
final fun androidx.compose.foundation/VerticalScrollbar(androidx.compose.foundation.v2/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.foundation/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/VerticalScrollbar|VerticalScrollbar(androidx.compose.foundation.v2.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.foundation.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/VerticalScrollbar(androidx.compose.foundation/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.foundation/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/VerticalScrollbar|VerticalScrollbar(androidx.compose.foundation.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.foundation.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop_getter|androidx_compose_foundation_BasicTooltipDefaults$stableprop_getter(){}[0] final fun androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop_getter|androidx_compose_foundation_BasicTooltipDefaults$stableprop_getter(){}[0]
final fun androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop_getter|androidx_compose_foundation_BorderStroke$stableprop_getter(){}[0] final fun androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop_getter|androidx_compose_foundation_BorderStroke$stableprop_getter(){}[0]
final fun androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop_getter|androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop_getter(){}[0] final fun androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop_getter|androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop_getter(){}[0]
@@ -2604,20 +2629,21 @@ final fun androidx.compose.foundation/androidx_compose_foundation_ComposeFoundat
final fun androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop_getter|androidx_compose_foundation_MarqueeDefaults$stableprop_getter(){}[0] final fun androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop_getter|androidx_compose_foundation_MarqueeDefaults$stableprop_getter(){}[0]
final fun androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop_getter|androidx_compose_foundation_MutatorMutex$stableprop_getter(){}[0] final fun androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop_getter|androidx_compose_foundation_MutatorMutex$stableprop_getter(){}[0]
final fun androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop_getter|androidx_compose_foundation_ScrollState$stableprop_getter(){}[0] final fun androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop_getter|androidx_compose_foundation_ScrollState$stableprop_getter(){}[0]
final fun androidx.compose.foundation/androidx_compose_foundation_ScrollbarStyle$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_ScrollbarStyle$stableprop_getter|androidx_compose_foundation_ScrollbarStyle$stableprop_getter(){}[0]
final fun androidx.compose.foundation/checkScrollableContainerConstraints(androidx.compose.ui.unit/Constraints, androidx.compose.foundation.gestures/Orientation) // androidx.compose.foundation/checkScrollableContainerConstraints|checkScrollableContainerConstraints(androidx.compose.ui.unit.Constraints;androidx.compose.foundation.gestures.Orientation){}[0] final fun androidx.compose.foundation/checkScrollableContainerConstraints(androidx.compose.ui.unit/Constraints, androidx.compose.foundation.gestures/Orientation) // androidx.compose.foundation/checkScrollableContainerConstraints|checkScrollableContainerConstraints(androidx.compose.ui.unit.Constraints;androidx.compose.foundation.gestures.Orientation){}[0]
final fun androidx.compose.foundation/defaultScrollbarStyle(): androidx.compose.foundation/ScrollbarStyle // androidx.compose.foundation/defaultScrollbarStyle|defaultScrollbarStyle(){}[0]
final fun androidx.compose.foundation/isSystemInDarkTheme(androidx.compose.runtime/Composer?, kotlin/Int): kotlin/Boolean // androidx.compose.foundation/isSystemInDarkTheme|isSystemInDarkTheme(androidx.compose.runtime.Composer?;kotlin.Int){}[0] final fun androidx.compose.foundation/isSystemInDarkTheme(androidx.compose.runtime/Composer?, kotlin/Int): kotlin/Boolean // androidx.compose.foundation/isSystemInDarkTheme|isSystemInDarkTheme(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun androidx.compose.foundation/rememberBasicTooltipState(kotlin/Boolean, kotlin/Boolean, androidx.compose.foundation/MutatorMutex?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation/BasicTooltipState // androidx.compose.foundation/rememberBasicTooltipState|rememberBasicTooltipState(kotlin.Boolean;kotlin.Boolean;androidx.compose.foundation.MutatorMutex?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation/rememberBasicTooltipState(kotlin/Boolean, kotlin/Boolean, androidx.compose.foundation/MutatorMutex?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation/BasicTooltipState // androidx.compose.foundation/rememberBasicTooltipState|rememberBasicTooltipState(kotlin.Boolean;kotlin.Boolean;androidx.compose.foundation.MutatorMutex?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/rememberOldScrollbarAdapter(androidx.compose.foundation.lazy/LazyListState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation/ScrollbarAdapter // androidx.compose.foundation/rememberOldScrollbarAdapter|rememberOldScrollbarAdapter(androidx.compose.foundation.lazy.LazyListState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun androidx.compose.foundation/rememberOldScrollbarAdapter(androidx.compose.foundation/ScrollState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation/ScrollbarAdapter // androidx.compose.foundation/rememberOldScrollbarAdapter|rememberOldScrollbarAdapter(androidx.compose.foundation.ScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun androidx.compose.foundation/rememberOverscrollEffect(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation/OverscrollEffect? // androidx.compose.foundation/rememberOverscrollEffect|rememberOverscrollEffect(androidx.compose.runtime.Composer?;kotlin.Int){}[0] final fun androidx.compose.foundation/rememberOverscrollEffect(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation/OverscrollEffect? // androidx.compose.foundation/rememberOverscrollEffect|rememberOverscrollEffect(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun androidx.compose.foundation/rememberScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation/ScrollState // androidx.compose.foundation/rememberScrollState|rememberScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun androidx.compose.foundation/rememberScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation/ScrollState // androidx.compose.foundation/rememberScrollState|rememberScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.foundation.lazy.grid/LazyGridState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.lazy.grid.LazyGridState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.foundation.lazy/LazyListState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.lazy.LazyListState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.foundation.text/TextFieldScrollState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.text.TextFieldScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.foundation/ScrollState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.ScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun com.compose.sdl.icons/IconFontIcon(kotlin/Int, kotlin/String, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/Dp, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.icons/IconFontIcon|IconFontIcon(kotlin.Int;kotlin.String;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.Dp;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun com.compose.sdl.icons/IconFontIcon(kotlin/Int, kotlin/String, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/Dp, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.icons/IconFontIcon|IconFontIcon(kotlin.Int;kotlin.String;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.Dp;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop_getter(): kotlin/Int // com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop_getter|com_compose_sdl_icons_IconDefaults$stableprop_getter(){}[0]
final fun com.compose.sdl.modifier/rememberMutableInteractionSource(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.interaction/MutableInteractionSource // com.compose.sdl.modifier/rememberMutableInteractionSource|rememberMutableInteractionSource(androidx.compose.runtime.Composer?;kotlin.Int){}[0] final fun com.compose.sdl.modifier/rememberMutableInteractionSource(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.interaction/MutableInteractionSource // com.compose.sdl.modifier/rememberMutableInteractionSource|rememberMutableInteractionSource(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun com.compose.sdl.scrollbar/HorizontalScrollbar(com.compose.sdl.scrollbar/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, com.compose.sdl.scrollbar/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.scrollbar/HorizontalScrollbar|HorizontalScrollbar(com.compose.sdl.scrollbar.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;com.compose.sdl.scrollbar.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.compose.sdl.scrollbar/VerticalScrollbar(com.compose.sdl.scrollbar/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, com.compose.sdl.scrollbar/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.scrollbar/VerticalScrollbar|VerticalScrollbar(com.compose.sdl.scrollbar.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;com.compose.sdl.scrollbar.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter(): kotlin/Int // com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter|com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter(){}[0]
final fun com.compose.sdl.scrollbar/defaultScrollbarStyle(): com.compose.sdl.scrollbar/ScrollbarStyle // com.compose.sdl.scrollbar/defaultScrollbarStyle|defaultScrollbarStyle(){}[0]
final fun com.compose.sdl.scrollbar/rememberScrollbarAdapter(androidx.compose.foundation.lazy/LazyListState, androidx.compose.runtime/Composer?, kotlin/Int): com.compose.sdl.scrollbar/ScrollbarAdapter // com.compose.sdl.scrollbar/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.lazy.LazyListState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun com.compose.sdl.scrollbar/rememberScrollbarAdapter(androidx.compose.foundation/ScrollState, androidx.compose.runtime/Composer?, kotlin/Int): com.compose.sdl.scrollbar/ScrollbarAdapter // com.compose.sdl.scrollbar/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.ScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign?, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign?;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign?, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign?;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.compose.sdl.widgets/HorizontalSplitPane(androidx.compose.ui/Modifier?, androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.widgets/HorizontalSplitPane|HorizontalSplitPane(androidx.compose.ui.Modifier?;androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun com.compose.sdl.widgets/HorizontalSplitPane(androidx.compose.ui/Modifier?, androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.widgets/HorizontalSplitPane|HorizontalSplitPane(androidx.compose.ui.Modifier?;androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
@@ -1,6 +1,6 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
// :foundation — androidx.compose.foundation.* AND androidx.compose.animation.* // :foundation - androidx.compose.foundation.* AND androidx.compose.animation.*
// (non-core) vendored VERBATIM from upstream. Sits on :ui (which still owns // (non-core) vendored VERBATIM from upstream. Sits on :ui (which still owns
// androidx.compose.ui.*, the Skia renderer, cinterops) + :animation-core. // androidx.compose.ui.*, the Skia renderer, cinterops) + :animation-core.
// //
@@ -14,12 +14,12 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
// module boundary is coalesced. // module boundary is coalesced.
// //
// Provenance = foundation/compose-fork.txt + scripts/compose-fork/compose.properties. // Provenance = foundation/compose-fork.txt + scripts/compose-fork/compose.properties.
// Never hand-edit files under foundation/src/vendor/ — change the manifest and // Never hand-edit files under foundation/src/vendor/ - change the manifest and
// re-run `bash scripts/compose-fork/sync.sh :foundation`. // re-run `bash scripts/compose-fork/sync.sh :foundation`.
// //
// Note on DarkTheme: foundation declares `internal expect fun _isSystemInDarkTheme()`, // Note on DarkTheme: foundation declares `internal expect fun _isSystemInDarkTheme()`,
// and its actual lives here as a plain nativeMain kt file (see // and its actual lives here as a plain nativeMain kt file (see
// src/nativeMain/kotlin/androidx/compose/foundation/DarkTheme.native.kt) — a single // src/nativeMain/kotlin/androidx/compose/foundation/DarkTheme.native.kt) - a single
// stub returning false. The upstream skiko per-renderer actuals were dropped // stub returning false. The upstream skiko per-renderer actuals were dropped
// when :foundation was split from :ui: expect/actual must live in the same // when :foundation was split from :ui: expect/actual must live in the same
// module, and duplicating the skikoRenderer source-set hierarchy on // module, and duplicating the skikoRenderer source-set hierarchy on
@@ -48,10 +48,10 @@ kotlin {
commonMain { commonMain {
kotlin.srcDir("src/vendor/common/kotlin") kotlin.srcDir("src/vendor/common/kotlin")
dependencies { dependencies {
api(project(":ui")) api(project(":compose:ui:ui"))
api(project(":animation-core")) api(project(":compose:animation:animation-core"))
api(project(":animation")) api(project(":compose:animation:animation"))
api(project(":foundation-layout")) api(project(":compose:foundation:foundation-layout"))
} }
} }
nativeMain { nativeMain {
@@ -356,7 +356,6 @@ commonMain/kotlin/ -> src/vendor/common/kotlin/
# | commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt -> src/vendor/common/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt # | commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt -> src/vendor/common/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt
# | commonMain/kotlin/androidx/compose/foundation/text/selection/TextSelectionColors.kt -> src/vendor/common/kotlin/androidx/compose/foundation/text/selection/TextSelectionColors.kt # | commonMain/kotlin/androidx/compose/foundation/text/selection/TextSelectionColors.kt -> src/vendor/common/kotlin/androidx/compose/foundation/text/selection/TextSelectionColors.kt
# | commonMain/kotlin/androidx/compose/foundation/text/selection/TextSelectionDelegate.kt -> src/vendor/common/kotlin/androidx/compose/foundation/text/selection/TextSelectionDelegate.kt # | commonMain/kotlin/androidx/compose/foundation/text/selection/TextSelectionDelegate.kt -> src/vendor/common/kotlin/androidx/compose/foundation/text/selection/TextSelectionDelegate.kt
!commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionLayout.kt
macosMain/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.macos.kt -> src/vendor/native/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.native.kt macosMain/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.macos.kt -> src/vendor/native/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.native.kt
@@ -1,810 +0,0 @@
@file:Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE", "LESS_VISIBLE_TYPE_ACCESS_IN_INLINE")
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// MANUAL VENDOR of upstream compose/foundation/foundation/src/commonMain/kotlin/
// androidx/compose/foundation/text/selection/SelectionLayout.kt.
// VENDOR-BASE: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionLayout.kt @ v1.12.0-beta03+dev4483
//
// Change vs upstream: MultiSelectionLayout.crossStatus and slotToIndex clamp their
// infoList index to the valid range. Upstream assumes that if the drag position never
// resolves ON or BEFORE any selectable, at least one slot is anchored on a text (odd).
// That assumption breaks when a mouse-down starts a NEW selection AFTER every selectable
// in the container — both slots stay UNASSIGNED and get defaulted to lastSlot = 2N by
// SelectionLayoutBuilder.build(), so startSlot / 2 == infoList.size (OOB). Repro:
// apidemo response viewer, drag-select in the empty area below the last body block.
package androidx.compose.foundation.text.selection
import androidx.collection.LongIntMap
import androidx.collection.LongObjectMap
import androidx.collection.MutableLongIntMap
import androidx.collection.buildLongObjectMap
import androidx.collection.emptyLongObjectMap
import androidx.collection.longObjectMapOf
import androidx.collection.mutableLongIntMapOf
import androidx.collection.mutableLongObjectMapOf
import androidx.compose.foundation.internal.checkPrecondition
import androidx.compose.foundation.text.selection.Direction.AFTER
import androidx.compose.foundation.text.selection.Direction.BEFORE
import androidx.compose.foundation.text.selection.Direction.ON
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
/**
* Selection data around how the pointer relates to the actual positions of the Text components.
*
* # Explanation of Slots
*
* The Slot value is meant to sit either *on* an index or *between* indices. The former means the
* pointer is on a `Text` (like slot value `1` and index `0` below). The latter means the pointer is
* not on a `Text`, but between `Text`s (like slot value `0` or `2` below). So a slot value of `2`
* means that the pointer is between the `Text`s at index `0` and `1`, perhaps in padding or a
* non-selectable `Text`.
*
* ```
* slot value 0 1 2 3 4 5 6 7 8 9 10
* info index 0 1 2 3 4
* ```
*
* ## Mappings:
* The `X` represents an impossible slot assignment The `|`, `/`, and `\` represent a slot mapping.
*
* ### Mapping minimum slot:
* ```
* slot value 0 1 2 3 4 5 6 7 8 9 10
* \ | \ | \ | \ | \ | X
* info index 0 1 2 3 4
*
* min-slot index = slot / 2
* ```
*
* Minimum slot cannot be after the final `Text` (index `4`).
*
* ### Mapping maximum slot:
* ```
* slot value 0 1 2 3 4 5 6 7 8 9 10
* X | / | / | / | / | /
* info index 0 1 2 3 4
* max-slot index = (slot - 1) / 2
* ```
*
* Maximum slot cannot be before the first `Text` (index `0`).
*
* ## Assertions
* * The non-dragging slot should always be directly on a text (odd) because the non-dragging handle
* must be anchored somewhere.
* * Because of this, we can determine that if `startSlot == endSlot` then it also follows that
* `startSlot` and `endSlot` are even.
*/
internal interface SelectionLayout {
/** The number of [SelectableInfo]s in this [SelectionLayout]. */
val size: Int
/** The slot of the start anchor. */
val startSlot: Int
/** The slot of the end anchor. */
val endSlot: Int
/** The [CrossStatus] of this layout as determined by the slot/offsets. */
val crossStatus: CrossStatus
/** The [SelectableInfo] that the start is on. */
val startInfo: SelectableInfo
/** The [SelectableInfo] that the end is on. */
val endInfo: SelectableInfo
/** The [SelectableInfo] that the start/end is on as determined by [isStartHandle]. */
val currentInfo: SelectableInfo
/** The [SelectableInfo] for the first selectable on the screen. */
val firstInfo: SelectableInfo
/** The [SelectableInfo] for the last selectable on the screen. */
val lastInfo: SelectableInfo
/**
* Run a function on every [SelectableInfo] between [firstInfo] and [lastInfo] (not including
* [firstInfo]/[lastInfo]).
*/
fun forEachMiddleInfo(block: (SelectableInfo) -> Unit)
/** Whether the start or end anchor is currently being moved. */
val isStartHandle: Boolean
/** The previous [Selection] that we are modifying. */
val previousSelection: Selection?
/** Returns the [SelectableInfo] of the selectable with the given id. */
fun infoForSelectable(selectableId: Long): SelectableInfo?
/**
* Whether this layout, compared to another layout, has any relevant changes that would require
* recomputing selection.
*
* @param other the selection layout to check for changes compared to this one
*/
fun shouldRecomputeSelection(other: SelectionLayout?): Boolean
/**
* Splits a selection into a Map of selectable ID to Selections limited to that selectable ID.
*
* @param selection The selection to turn into subSelections
*/
fun createSubSelections(selection: Selection): LongObjectMap<Selection>
}
private class MultiSelectionLayout(
val selectableIdToInfoListIndex: LongIntMap,
val infoList: List<SelectableInfo>,
override val startSlot: Int,
override val endSlot: Int,
override val isStartHandle: Boolean,
override val previousSelection: Selection?,
) : SelectionLayout {
init {
checkPrecondition(infoList.size > 1) {
"MultiSelectionLayout requires an infoList size greater than 1, was ${infoList.size}."
}
}
private var _infoBySelectableId: LongObjectMap<SelectableInfo>? = null
private val infoBySelectableId: LongObjectMap<SelectableInfo>
get() {
var result = _infoBySelectableId
if (result == null) {
result = buildLongObjectMap { infoList.fastForEach { put(it.selectableId, it) } }
_infoBySelectableId = result
}
return result
}
// Most of these properties are unused unless shouldRecomputeSelection returns true,
// hence why getters are used everywhere.
override val size
get() = infoList.size
override val crossStatus: CrossStatus
get() =
when {
startSlot < endSlot -> CrossStatus.NOT_CROSSED
startSlot > endSlot -> CrossStatus.CROSSED
// FORK CHANGE: clamp when the gesture starts past every selectable, both slots
// default to lastSlot = 2 * infoList.size, so slot / 2 would be out of bounds.
else -> infoList[(startSlot / 2).coerceAtMost(infoList.lastIndex)].rawCrossStatus
}
override val startInfo: SelectableInfo
get() = infoList[startOrEndSlotToIndex(startSlot, isStartSlot = true)]
override val endInfo: SelectableInfo
get() = infoList[startOrEndSlotToIndex(endSlot, isStartSlot = false)]
override val currentInfo: SelectableInfo
get() = if (isStartHandle) startInfo else endInfo
override val firstInfo: SelectableInfo
get() = if (crossStatus == CrossStatus.CROSSED) endInfo else startInfo
override val lastInfo: SelectableInfo
get() = if (crossStatus == CrossStatus.CROSSED) startInfo else endInfo
override fun forEachMiddleInfo(block: (SelectableInfo) -> Unit) {
val minIndex = getInfoListIndexBySelectableId(firstInfo.selectableId)
val maxIndex = getInfoListIndexBySelectableId(lastInfo.selectableId)
if (minIndex + 1 >= maxIndex) {
return
}
for (i in minIndex + 1 until maxIndex) {
block(infoList[i])
}
}
override fun infoForSelectable(selectableId: Long): SelectableInfo? {
return infoBySelectableId[selectableId]
}
override fun shouldRecomputeSelection(other: SelectionLayout?): Boolean =
previousSelection == null ||
other == null ||
other !is MultiSelectionLayout ||
isStartHandle != other.isStartHandle ||
startSlot != other.startSlot ||
endSlot != other.endSlot ||
shouldAnyInfoRecomputeSelection(other)
private fun shouldAnyInfoRecomputeSelection(other: MultiSelectionLayout): Boolean {
if (size != other.size) return true
for (i in infoList.indices) {
val thisInfo = infoList[i]
val otherInfo = other.infoList[i]
if (thisInfo.shouldRecomputeSelection(otherInfo)) {
return true
}
}
return false
}
override fun createSubSelections(selection: Selection): LongObjectMap<Selection> =
createSubSelections(
selection = selection,
sortedItems = infoList,
getId = { it.selectableId },
getSelectAll = { info, _ ->
if (selection.handlesCrossed) {
info.makeSingleLayoutSelection(info.textLength, 0)
} else {
info.makeSingleLayoutSelection(0, info.textLength)
}
},
createBoundarySelection = { info, isStart, offset, _ ->
val isCrossed = selection.handlesCrossed
if (isStart) {
if (isCrossed) {
info.makeSingleLayoutSelection(start = offset, end = 0)
} else {
info.makeSingleLayoutSelection(start = offset, end = info.textLength)
}
} else {
if (isCrossed) {
info.makeSingleLayoutSelection(start = info.textLength, end = offset)
} else {
info.makeSingleLayoutSelection(start = 0, end = offset)
}
}
},
)
override fun toString(): String =
"MultiSelectionLayout(isStartHandle=$isStartHandle, " +
"startPosition=${(startSlot + 1).toFloat() / 2}, " +
"endPosition=${(endSlot + 1).toFloat() / 2}, " +
"crossed=$crossStatus, " +
"infos=${
buildString {
append("[\n\t")
var first = true
infoList
.fastForEachIndexed { index, info ->
if (first) {
first = false
} else {
append(",\n\t")
}
append("${(index + 1)} -> $info")
}
append("\n]")
}
})"
private fun startOrEndSlotToIndex(slot: Int, isStartSlot: Boolean): Int =
slotToIndex(
slot = slot,
isMinimumSlot =
when (crossStatus) {
// collapsed: doesn't matter whether true or false, it will result in the same
// index
CrossStatus.COLLAPSED -> true
CrossStatus.NOT_CROSSED -> isStartSlot
CrossStatus.CROSSED -> !isStartSlot
},
)
private fun slotToIndex(slot: Int, isMinimumSlot: Boolean): Int {
val slotAdjustment = if (isMinimumSlot) 0 else 1
// FORK CHANGE: clamp when the gesture starts past every selectable, slots default
// to lastSlot = 2 * infoList.size, so slot / 2 would be out of bounds.
return ((slot - slotAdjustment) / 2).coerceIn(0, infoList.lastIndex)
}
private fun getInfoListIndexBySelectableId(id: Long): Int =
try {
selectableIdToInfoListIndex[id]
} catch (e: NoSuchElementException) {
throw IllegalStateException("Invalid selectableId: $id", e)
}
}
/**
* Create a selection layout that has only one slot.
*
* @param isStartHandle whether this is the start or end anchor
* @param previousSelection the previous selection
* @param info the single [SelectableInfo]
*/
private class SingleSelectionLayout(
override val isStartHandle: Boolean,
override val startSlot: Int,
override val endSlot: Int,
override val previousSelection: Selection?,
private val info: SelectableInfo,
) : SelectionLayout {
companion object {
const val DEFAULT_SLOT = 1
const val DEFAULT_SELECTABLE_ID = 1L
}
override val size
get() = 1
override val crossStatus: CrossStatus
get() =
when {
startSlot < endSlot -> CrossStatus.NOT_CROSSED
startSlot > endSlot -> CrossStatus.CROSSED
else -> info.rawCrossStatus
}
override val startInfo: SelectableInfo
get() = info
override val endInfo: SelectableInfo
get() = info
override val currentInfo: SelectableInfo
get() = info
override val firstInfo: SelectableInfo
get() = info
override val lastInfo: SelectableInfo
get() = info
override fun forEachMiddleInfo(block: (SelectableInfo) -> Unit) {
// there are no middle infos, so do nothing
}
override fun infoForSelectable(selectableId: Long): SelectableInfo? {
return if (selectableId == info.selectableId) info else null
}
override fun shouldRecomputeSelection(other: SelectionLayout?): Boolean =
previousSelection == null ||
other == null ||
other !is SingleSelectionLayout ||
startSlot != other.startSlot ||
endSlot != other.endSlot ||
isStartHandle != other.isStartHandle ||
info.shouldRecomputeSelection(other.info)
override fun createSubSelections(selection: Selection): LongObjectMap<Selection> {
val finalSelection =
selection.run {
// uncross handles if necessary
if (
(!handlesCrossed && start.offset > end.offset) ||
(handlesCrossed && start.offset <= end.offset)
) {
copy(handlesCrossed = !handlesCrossed)
} else {
this
}
}
return longObjectMapOf(info.selectableId, finalSelection)
}
override fun toString(): String =
"SingleSelectionLayout(isStartHandle=$isStartHandle, crossed=$crossStatus, info=\n\t$info)"
}
/**
* Create a selection layout that has only one slot.
*
* This is intended for TextField, where multiple selectables is of no concern.
*
* @param layoutResult the [TextLayoutResult] for the text field
* @param rawStartHandleOffset the index of the start handle
* @param rawEndHandleOffset the index of the end handle
* @param rawPreviousHandleOffset the previous handle offset based on [isStartHandle], or
* [UNASSIGNED_SLOT] if none
* @param previousSelectionRange the previous selection
* @param isStartOfSelection whether this is the start of a selection gesture (no previous context)
* @param isStartHandle whether this is the start or end anchor
*/
internal fun getTextFieldSelectionLayout(
layoutResult: TextLayoutResult,
rawStartHandleOffset: Int,
rawEndHandleOffset: Int,
rawPreviousHandleOffset: Int,
previousSelectionRange: TextRange,
isStartOfSelection: Boolean,
isStartHandle: Boolean,
): SelectionLayout =
SingleSelectionLayout(
isStartHandle = isStartHandle,
startSlot = SingleSelectionLayout.DEFAULT_SLOT,
endSlot = SingleSelectionLayout.DEFAULT_SLOT,
previousSelection =
if (isStartOfSelection) null
else
Selection(
start =
Selection.AnchorInfo(
layoutResult.getTextDirectionForOffset(previousSelectionRange.start),
previousSelectionRange.start,
SingleSelectionLayout.DEFAULT_SELECTABLE_ID,
),
end =
Selection.AnchorInfo(
layoutResult.getTextDirectionForOffset(previousSelectionRange.end),
previousSelectionRange.end,
SingleSelectionLayout.DEFAULT_SELECTABLE_ID,
),
handlesCrossed = previousSelectionRange.reversed,
),
info =
SelectableInfo(
selectableId = SingleSelectionLayout.DEFAULT_SELECTABLE_ID,
slot = SingleSelectionLayout.DEFAULT_SLOT,
rawStartHandleOffset = rawStartHandleOffset,
rawEndHandleOffset = rawEndHandleOffset,
textLayoutResult = layoutResult,
rawPreviousHandleOffset = rawPreviousHandleOffset,
),
)
/** Whether something is crossed as determined by the position of the start/end. */
internal enum class CrossStatus {
/** The start comes after the end. */
CROSSED,
/** The start comes before the end. */
NOT_CROSSED,
/** The start is the same as the end. */
COLLAPSED,
}
/** Slot has not been assigned yet */
internal const val UNASSIGNED_SLOT = -1
/**
* A builder for [SelectionLayout] that ensures the data structures and slots are properly
* constructed.
*
* @param previousHandlePosition the previous handle position matching the handle directed to by
* [isStartHandle]
* @param containerCoordinates the coordinates of the [SelectionContainer] for converting
* [SelectionContainer] coordinates to their respective [Selectable] coordinates
* @param isStartHandle whether the currently pressed/clicked handle is the start
* @param previousLayout The [SelectionLayout] created when [currentPosition] was
* [previousHandlePosition]. When non-null, it is used to obtain
* [SelectableInfo.rawPreviousHandleOffset] instead of re-computing it from
* [previousHandlePosition]. Note that this is not just an optimization. When selectables are
* moved (due to drag-to-scroll, for example) inside the selection container, it becomes
* impossible to correctly deduce [SelectableInfo.rawPreviousHandleOffset] from
* [previousHandlePosition] (because to do that, the "previous" layout coordinates are needed, but
* are not available).
* @param selectableIdOrderingComparator determines the ordering of selectables by their IDs
* @param allowSelectionBetweenSelectables whether selection in the "empty" area where there are no
* selectables is allowed.
*/
internal class SelectionLayoutBuilder(
val currentPosition: Offset,
val previousHandlePosition: Offset,
val containerCoordinates: LayoutCoordinates,
val isStartHandle: Boolean,
val previousSelection: Selection?,
val previousLayout: SelectionLayout?,
val selectableIdOrderingComparator: Comparator<Long>,
val allowSelectionBetweenSelectables: Boolean = false,
) {
private val selectableIdToInfoListIndex: MutableLongIntMap = mutableLongIntMapOf()
private val infoList: MutableList<SelectableInfo> = mutableListOf()
private var startSlot: Int = UNASSIGNED_SLOT
private var endSlot: Int = UNASSIGNED_SLOT
private var currentSlot: Int = UNASSIGNED_SLOT
/**
* Finishes building the [SelectionLayout] and returns it.
*
* @return the [SelectionLayout] or null if no [SelectableInfo]s were added.
*/
fun build(): SelectionLayout? {
val lastSlot = currentSlot + 1
return when (infoList.size) {
0 -> {
null
}
1 -> {
SingleSelectionLayout(
info = infoList.single(),
startSlot = if (startSlot == UNASSIGNED_SLOT) lastSlot else startSlot,
endSlot = if (endSlot == UNASSIGNED_SLOT) lastSlot else endSlot,
previousSelection = previousSelection,
isStartHandle = isStartHandle,
)
}
else -> {
MultiSelectionLayout(
selectableIdToInfoListIndex = selectableIdToInfoListIndex,
infoList = infoList,
startSlot = if (startSlot == UNASSIGNED_SLOT) lastSlot else startSlot,
endSlot = if (endSlot == UNASSIGNED_SLOT) lastSlot else endSlot,
isStartHandle = isStartHandle,
previousSelection = previousSelection,
)
}
}
}
/** Appends a selection info to this builder. */
fun appendInfo(
selectableId: Long,
rawStartHandleOffset: Int,
startXHandleDirection: Direction,
startYHandleDirection: Direction,
rawEndHandleOffset: Int,
endXHandleDirection: Direction,
endYHandleDirection: Direction,
rawPreviousHandleOffset: Int,
textLayoutResult: TextLayoutResult,
): SelectableInfo {
// We need currentSlot to equal the slot of the "last" info when getLayout is called,
// so increment this before adding the info and leave the correct slot in place at the end.
currentSlot += 2
val selectableInfo =
SelectableInfo(
selectableId = selectableId,
slot = currentSlot,
rawStartHandleOffset = rawStartHandleOffset,
rawEndHandleOffset = rawEndHandleOffset,
rawPreviousHandleOffset = rawPreviousHandleOffset,
textLayoutResult = textLayoutResult,
)
startSlot = updateSlot(startSlot, startXHandleDirection, startYHandleDirection)
endSlot = updateSlot(endSlot, endXHandleDirection, endYHandleDirection)
selectableIdToInfoListIndex[selectableId] = infoList.size
infoList += selectableInfo
return selectableInfo
}
/**
* Find the slot for a selectable given the current position's directions from the selectable.
*
* The selectables must be ordered in the order in which they would be selected, and then this
* function should be called for each of those selectables.
*
* It is expected that the input [slot] is also assigned the result of this function.
*
* This function is stateful.
*
* @param slot the current value of this slot.
* @param xPositionDirection Where the x-position is relative to the selectable
* @param yPositionDirection Where the y-position is relative to the selectable
*/
private fun updateSlot(
slot: Int,
xPositionDirection: Direction,
yPositionDirection: Direction,
): Int {
if (slot != UNASSIGNED_SLOT) {
// don't overwrite if the slot has already been determined
return slot
}
// slot has not been determined yet,
// see if we are on or past the selectable we are looking for
return when (resolve2dDirection(xPositionDirection, yPositionDirection)) {
// If we get here, that means we never found a selectable that contains our gesture
// position. This is the first selectable that is after the position,
// so our slot must be between the previous and current selectables.
BEFORE -> currentSlot - 1
// The gesture position is directly on this selectable, so use this one.
ON -> currentSlot
// keep looking
AFTER -> slot
}
}
}
/** Where the position of a cursor/press is compared to a selectable. */
internal enum class Direction {
/** The cursor/press is before the selectable */
BEFORE,
/** The cursor/press is on the selectable */
ON,
/** The cursor/press is after the selectable */
AFTER,
}
/**
* Determine direction based on an x/y direction.
*
* This will use the [y] direction unless it is [ON], in which case it will use the [x] direction.
*/
internal fun resolve2dDirection(x: Direction, y: Direction): Direction =
when (y) {
BEFORE -> BEFORE
ON ->
when (x) {
BEFORE -> BEFORE
ON -> ON
AFTER -> AFTER
}
AFTER -> AFTER
}
/** Data about a specific selectable within a [SelectionLayout]. */
internal class SelectableInfo(
val selectableId: Long,
val slot: Int,
val rawStartHandleOffset: Int,
val rawEndHandleOffset: Int,
val rawPreviousHandleOffset: Int,
val textLayoutResult: TextLayoutResult,
) {
/** The [String] in the selectable. */
val inputText: String
get() = textLayoutResult.layoutInput.text.text
/** The length of the [String] in the selectable. */
val textLength: Int
get() = inputText.length
/** Whether the raw offsets of this info are crossed. */
val rawCrossStatus: CrossStatus
get() =
when {
rawStartHandleOffset < rawEndHandleOffset -> CrossStatus.NOT_CROSSED
rawStartHandleOffset > rawEndHandleOffset -> CrossStatus.CROSSED
else -> CrossStatus.COLLAPSED
}
private val startRunDirection
get() = textLayoutResult.getTextDirectionForOffset(rawStartHandleOffset)
private val endRunDirection
get() = textLayoutResult.getTextDirectionForOffset(rawEndHandleOffset)
/**
* Whether this info, compared to another info, has any relevant changes that would require
* recomputing selection.
*
* @param other the selectable info to check for changes compared to this one
*/
fun shouldRecomputeSelection(other: SelectableInfo): Boolean =
selectableId != other.selectableId ||
rawStartHandleOffset != other.rawStartHandleOffset ||
rawEndHandleOffset != other.rawEndHandleOffset
/** Get a [Selection.AnchorInfo] for this [SelectableInfo] at the given [offset]. */
fun anchorForOffset(offset: Int): Selection.AnchorInfo =
Selection.AnchorInfo(
direction = textLayoutResult.getTextDirectionForOffset(offset),
offset = offset,
selectableId = selectableId,
)
/**
* Get a [Selection] within the selectable represented by this [SelectableInfo] for the given
* [start] and [end] offsets.
*/
fun makeSingleLayoutSelection(start: Int, end: Int): Selection =
Selection(
start = anchorForOffset(start),
end = anchorForOffset(end),
handlesCrossed = start > end,
)
override fun toString(): String =
"SelectionInfo(id=$selectableId, " +
"range=($rawStartHandleOffset-$startRunDirection,$rawEndHandleOffset-$endRunDirection), " +
"prevOffset=$rawPreviousHandleOffset)"
}
/**
* Verify that the selection is truly collapsed.
*
* If the selection is contained within one selectable, this simply checks if the offsets are equal.
*
* If the Selection spans multiple selectables, then this will verify that every selected selectable
* contains a zero-width selection.
*/
internal fun Selection?.isCollapsed(layout: SelectionLayout?): Boolean {
this ?: return true
layout ?: return true
// Selection is within one selectable, simply check if the offsets are the same.
if (start.selectableId == end.selectableId) {
return start.offset == end.offset
}
// check that maxAnchor offset is 0, else the selection cannot be collapsed.
val maxAnchor = if (handlesCrossed) start else end
if (maxAnchor.offset != 0) {
return false
}
// check that the minAnchor offset is equal to the length of the text,
// else the selection is not collapsed
val minAnchor = if (handlesCrossed) end else start
if (layout.firstInfo.textLength != minAnchor.offset) {
return false
}
// Every selectable between the min and max must have empty text,
// else there is some text selected.
var allTextsEmpty = true
layout.forEachMiddleInfo {
if (it.inputText.isNotEmpty()) {
allTextsEmpty = false
}
}
return allTextsEmpty
}
internal fun <T> createSubSelections(
selection: Selection,
sortedItems: List<T>,
getId: (T) -> Long,
getSelectAll: (T, handlesCrossed: Boolean) -> Selection?,
createBoundarySelection:
(item: T, isStart: Boolean, offset: Int, handlesCrossed: Boolean) -> Selection?,
): LongObjectMap<Selection> {
val startId = selection.start.selectableId
val endId = selection.end.selectableId
if (startId == endId) {
return longObjectMapOf(startId, selection)
}
val startIndex = sortedItems.indexOfFirst { getId(it) == startId }
val endIndex = sortedItems.indexOfFirst { getId(it) == endId }
if (startIndex == -1 || endIndex == -1) {
return emptyLongObjectMap()
}
val firstIndex = minOf(startIndex, endIndex)
val lastIndex = maxOf(startIndex, endIndex)
val handlesCrossed = startIndex > endIndex
val result = mutableLongObjectMapOf<Selection>()
for (i in firstIndex..lastIndex) {
val item = sortedItems[i]
val id = getId(item)
val subSelection =
when (id) {
startId ->
createBoundarySelection(item, true, selection.start.offset, handlesCrossed)
endId -> createBoundarySelection(item, false, selection.end.offset, handlesCrossed)
else -> getSelectAll(item, handlesCrossed)
}
subSelection?.let { result[id] = it }
}
return result
}
@@ -10,7 +10,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
// ================== // ==================
// MARK: IconText — icon-font text on the standard text path // MARK: IconText - icon-font text on the standard text path
// ================== // ==================
/** /**
@@ -21,7 +21,7 @@ import androidx.compose.ui.unit.TextUnit
Now just a [BasicText]: the icon font family and its variable axes are threaded Now just a [BasicText]: the icon font family and its variable axes are threaded
through [namedFontFamily] (`axes = …`), which the skiko text engine reads via through [namedFontFamily] (`axes = …`), which the skiko text engine reads via
`FontFamily.projectFontVariations()` and applies to the typeface. So icons `FontFamily.projectFontVariations()` and applies to the typeface. So icons
measure + draw through the same skiko `skparagraph` path as ordinary text — no measure + draw through the same skiko `skparagraph` path as ordinary text - no
separate renderer/measurer seam. separate renderer/measurer seam.
Material `Icon(codepoint = …, fontFamily = …)` uses this; Text / BasicText / Material `Icon(codepoint = …, fontFamily = …)` uses this; Text / BasicText /
@@ -15,7 +15,7 @@ import androidx.compose.ui.unit.IntOffset
/** One selection-highlight rectangle (in layout pixels), painted behind the glyphs. /** One selection-highlight rectangle (in layout pixels), painted behind the glyphs.
Internal render glue shared by BasicText (selection-aware) and BasicTextField. Internal render glue shared by BasicText (selection-aware) and BasicTextField.
Sizes come from Paragraph.getBoundingBox / getPathForRange — already pixels Sizes come from Paragraph.getBoundingBox / getPathForRange - already pixels
(see § HiDPI in CLAUDE.md), so use the pixel-based offset + a raw Layout (see § HiDPI in CLAUDE.md), so use the pixel-based offset + a raw Layout
sizing shim instead of `.dp` (which would double-scale on Retina). */ sizing shim instead of `.dp` (which would double-scale on Retina). */
@Composable @Composable
@@ -73,7 +73,7 @@ fun HorizontalSplitPane(
Box(modifier = Modifier.width(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxHeight()) { first() } Box(modifier = Modifier.width(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxHeight()) { first() }
// Solid divider that fills its own slot — what you see is exactly what // Solid divider that fills its own slot - what you see is exactly what
// you can grab. Hover or an in-progress drag just changes the colour (no // you can grab. Hover or an in-progress drag just changes the colour (no
// size change → no layout shift, and no flicker when the pointer briefly // size change → no layout shift, and no flicker when the pointer briefly
// leaves the slot mid-drag). // leaves the slot mid-drag).

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